From 1555258b9ad18cda32cf209c8e13440cc026b065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= <143604440+tvtphuc-axonivy@users.noreply.github.com> Date: Wed, 29 May 2024 15:42:13 +0700 Subject: [PATCH 01/62] MARP-275 Create new BE project (#4) * MARP-275 Create new BE project * User MockitoJUnitRunner instead of SpringRunner * RHT-275 Handle For SonaQube * MARP-275 Add lombok.config --------- --- .github/workflows/ci-build.yml | 59 +++++++++++++++ .github/workflows/dev-build.yml | 5 +- .github/workflows/sonarqube.yml | 23 ------ .gitignore | 33 +++++++++ README.md | 35 +++++++++ lombok.config | 2 + pom.xml | 74 +++++++++++++++++++ .../market/MarketplaceServiceApplication.java | 13 ++++ .../axonivy/market/ServletInitializer.java | 13 ++++ .../market/config/HeaderInterceptor.java | 25 +++++++ .../axonivy/market/config/MongoConfig.java | 36 +++++++++ .../axonivy/market/config/OpenApiConfig.java | 26 +++++++ .../com/axonivy/market/config/WebConfig.java | 22 ++++++ .../market/constants/DocumentConstants.java | 5 ++ .../constants/ErrorMessageConstants.java | 5 ++ .../constants/RequestMappingConstants.java | 5 ++ .../market/controller/UserController.java | 28 +++++++ .../java/com/axonivy/market/entity/User.java | 21 ++++++ .../market/exceptions/ExceptionHandlers.java | 19 +++++ .../exceptions/MissingHeaderException.java | 12 +++ .../com/axonivy/market/model/ApiError.java | 15 ++++ .../market/repository/UserRepository.java | 9 +++ .../axonivy/market/service/UserService.java | 9 +++ .../market/service/impl/UserServiceImpl.java | 24 ++++++ src/main/resources/application.properties | 6 ++ .../market/service/UserServiceImplTest.java | 42 +++++++++++ 26 files changed, 539 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/ci-build.yml delete mode 100644 .github/workflows/sonarqube.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lombok.config create mode 100644 pom.xml create mode 100644 src/main/java/com/axonivy/market/MarketplaceServiceApplication.java create mode 100644 src/main/java/com/axonivy/market/ServletInitializer.java create mode 100644 src/main/java/com/axonivy/market/config/HeaderInterceptor.java create mode 100644 src/main/java/com/axonivy/market/config/MongoConfig.java create mode 100644 src/main/java/com/axonivy/market/config/OpenApiConfig.java create mode 100644 src/main/java/com/axonivy/market/config/WebConfig.java create mode 100644 src/main/java/com/axonivy/market/constants/DocumentConstants.java create mode 100644 src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java create mode 100644 src/main/java/com/axonivy/market/constants/RequestMappingConstants.java create mode 100644 src/main/java/com/axonivy/market/controller/UserController.java create mode 100644 src/main/java/com/axonivy/market/entity/User.java create mode 100644 src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java create mode 100644 src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java create mode 100644 src/main/java/com/axonivy/market/model/ApiError.java create mode 100644 src/main/java/com/axonivy/market/repository/UserRepository.java create mode 100644 src/main/java/com/axonivy/market/service/UserService.java create mode 100644 src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/axonivy/market/service/UserServiceImplTest.java diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 000000000..04a0a85c7 --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,59 @@ +name: CI Build +run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} + +on: + push: + workflow_dispatch: + +jobs: + analysis: + name: Sonarqube analysis + runs-on: self-hosted + env: + SONAR_PROJECT_KEY: "AxonIvy-Market-Service" + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Build with Maven + run: mvn clean install + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + with: + args: + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + -Dsonar.java.binaries=target/classes + - name: SonarQube Quality Gate check + id: sonarqube-quality-gate-check + uses: sonarsource/sonarqube-quality-gate-action@master + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + with: + args: + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + build: + name: Executes Tests + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Tests with Maven + run: mvn -B test --file pom.xml diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index ca9c398ba..63418eb2b 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -8,7 +8,7 @@ on: jobs: build: - + name: Packge project and deploy to tomcat runs-on: self-hosted steps: @@ -22,6 +22,3 @@ jobs: - name: Build with Maven run: mvn -B package --file pom.xml - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index 0c0d5b4e1..000000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: SonarQube Build -run-name: Analyze and upload reports to Octopus-Sonar by ${{github.actor}} - -on: - push: - branches: [ "develop" ] - workflow_dispatch: - -jobs: - Analysis: - runs-on: self-hosted - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - with: - args: - -Dsonar.projectKey=AxonIvy-Market-Service diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0384b84cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +/bin/ diff --git a/README.md b/README.md new file mode 100644 index 000000000..6682bcb6c --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.2.5/maven-plugin/reference/html/) +* [Spring Data MongoDB](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#data.nosql.mongodb) +* [Spring Web](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#web) + +### Guides +The following guides illustrate how to use some features concretely: + +* installing mongodb , and access it mongodb://localhost:27017/ in mongodb compass or studio3T +* run "mvn clean install" to build a project +* run "mvn test" to test all Test class + +### MongoDB's property configs +* We can set up properties in class application.properties and MongoConfig + +### Access Swagger URL: http://{your-host}/swagger-ui/index.html + +### Steps to set up: +* Installing mongodb, and access it as Url mongodb://localhost:27017/, and you can create and name whatever you want ,then you should put them to application.properties +* Run mvn clean install to build project +* Run mvn test to test all tests +* You can change the configuration in file “application.properties“ + +### In case of using eclipse you should install manually Lombok . +* Download lombok here Download +* run command java -jar lombok.jar and restart the eclipse then you can access file “eclipse.ini“ in eclipse folder where you install → there is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are successful +* Import the project then in the eclipse , you should run the command “mvn clean install“ +* After that you go to class MarketplaceServiceApplication → right click to main method → click run as → choose Java Application +* Then you can send a request in postman :) +* If you want to run single test in class UserServiceImplTest. You can right-click to method testFindAllUser and right click → select Run as → choose JUnit Test \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 000000000..a23edb413 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..bf1326f7b --- /dev/null +++ b/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.axonivy.market + marketplace-service + 0.0.1-SNAPSHOT + war + marketplace-service + marketplace-service + + 17 + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java new file mode 100644 index 000000000..f390c8fa3 --- /dev/null +++ b/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -0,0 +1,13 @@ +package com.axonivy.market; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MarketplaceServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MarketplaceServiceApplication.class, args); + } + +} diff --git a/src/main/java/com/axonivy/market/ServletInitializer.java b/src/main/java/com/axonivy/market/ServletInitializer.java new file mode 100644 index 000000000..378959608 --- /dev/null +++ b/src/main/java/com/axonivy/market/ServletInitializer.java @@ -0,0 +1,13 @@ +package com.axonivy.market; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(MarketplaceServiceApplication.class); + } + +} diff --git a/src/main/java/com/axonivy/market/config/HeaderInterceptor.java b/src/main/java/com/axonivy/market/config/HeaderInterceptor.java new file mode 100644 index 000000000..56a845abb --- /dev/null +++ b/src/main/java/com/axonivy/market/config/HeaderInterceptor.java @@ -0,0 +1,25 @@ +package com.axonivy.market.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.axonivy.market.exceptions.MissingHeaderException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class HeaderInterceptor implements HandlerInterceptor { + + @Value("${request.header}") + private String requestHeader; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (!requestHeader.equals(request.getHeader("X-Requested-By"))) { + throw new MissingHeaderException(); + } + return true; + } +} diff --git a/src/main/java/com/axonivy/market/config/MongoConfig.java b/src/main/java/com/axonivy/market/config/MongoConfig.java new file mode 100644 index 000000000..36b7b6f1d --- /dev/null +++ b/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -0,0 +1,36 @@ +package com.axonivy.market.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +@Configuration +@EnableMongoRepositories(basePackages = "com.axonivy.market.repository") +public class MongoConfig extends AbstractMongoClientConfiguration { + + @Value("${spring.data.mongodb.host}") + private String host; + + @Value("${spring.data.mongodb.database}") + private String databaseName; + + @Override + protected String getDatabaseName() { + return databaseName; + } + + @Override + public MongoClient mongoClient() { + ConnectionString connectionString = new ConnectionString(host); + MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString) + .build(); + + return MongoClients.create(mongoClientSettings); + } +} diff --git a/src/main/java/com/axonivy/market/config/OpenApiConfig.java b/src/main/java/com/axonivy/market/config/OpenApiConfig.java new file mode 100644 index 000000000..df02c02f3 --- /dev/null +++ b/src/main/java/com/axonivy/market/config/OpenApiConfig.java @@ -0,0 +1,26 @@ +package com.axonivy.market.config; + +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; + +@Configuration +public class OpenApiConfig { + @Bean + public GroupedOpenApi customHeaderOpenApi() { + return GroupedOpenApi.builder().group("default").addOpenApiCustomizer(customGlobalHeaders()).build(); + } + + private OpenApiCustomizer customGlobalHeaders() { + return openApi -> openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> { + Parameter headerParameter = new Parameter().in("header").schema(new StringSchema()).name("X-Requested-By") + .description("ivy").required(true); + + operation.addParametersItem(headerParameter); + })); + } +} diff --git a/src/main/java/com/axonivy/market/config/WebConfig.java b/src/main/java/com/axonivy/market/config/WebConfig.java new file mode 100644 index 000000000..f26adc247 --- /dev/null +++ b/src/main/java/com/axonivy/market/config/WebConfig.java @@ -0,0 +1,22 @@ +package com.axonivy.market.config; + +import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final HeaderInterceptor headerInterceptor; + + public WebConfig(HeaderInterceptor headerInterceptor) { + this.headerInterceptor = headerInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(headerInterceptor).addPathPatterns(USER_MAPPING); + } +} diff --git a/src/main/java/com/axonivy/market/constants/DocumentConstants.java b/src/main/java/com/axonivy/market/constants/DocumentConstants.java new file mode 100644 index 000000000..ef95831f2 --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/DocumentConstants.java @@ -0,0 +1,5 @@ +package com.axonivy.market.constants; + +public class DocumentConstants { + public static final String USER_DOCUMENT = "User"; +} diff --git a/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java b/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java new file mode 100644 index 000000000..7796172fd --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java @@ -0,0 +1,5 @@ +package com.axonivy.market.constants; + +public class ErrorMessageConstants { + public static final String INVALID_MISSING_HEADER_ERROR_MESSAGE = "Invalid or missing header"; +} diff --git a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java new file mode 100644 index 000000000..a0f9d15ac --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -0,0 +1,5 @@ +package com.axonivy.market.constants; + +public class RequestMappingConstants { + public static final String USER_MAPPING = "/user"; +} diff --git a/src/main/java/com/axonivy/market/controller/UserController.java b/src/main/java/com/axonivy/market/controller/UserController.java new file mode 100644 index 000000000..f94d46920 --- /dev/null +++ b/src/main/java/com/axonivy/market/controller/UserController.java @@ -0,0 +1,28 @@ +package com.axonivy.market.controller; + +import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; + +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.axonivy.market.entity.User; +import com.axonivy.market.service.UserService; + +@RestController +@RequestMapping(USER_MAPPING) +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public ResponseEntity> getAllUser() { + return ResponseEntity.ok(userService.getAllUsers()); + } +} diff --git a/src/main/java/com/axonivy/market/entity/User.java b/src/main/java/com/axonivy/market/entity/User.java new file mode 100644 index 000000000..f6035ac00 --- /dev/null +++ b/src/main/java/com/axonivy/market/entity/User.java @@ -0,0 +1,21 @@ +package com.axonivy.market.entity; + +import static com.axonivy.market.constants.DocumentConstants.USER_DOCUMENT; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@Document(USER_DOCUMENT) +public class User { + @Id + private String id; + private String username; + private String password; +} diff --git a/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java new file mode 100644 index 000000000..7359bd18b --- /dev/null +++ b/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -0,0 +1,19 @@ +package com.axonivy.market.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import com.axonivy.market.model.ApiError; + +@ControllerAdvice +public class ExceptionHandlers extends ResponseEntityExceptionHandler { + + @ExceptionHandler(MissingHeaderException.class) + protected ResponseEntity handleMissingServletRequestParameter(MissingHeaderException missingHeaderException) { + ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, missingHeaderException.getMessage()); + return new ResponseEntity<>(apiError, apiError.getStatus()); + } +} diff --git a/src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java b/src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java new file mode 100644 index 000000000..7c14e8380 --- /dev/null +++ b/src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java @@ -0,0 +1,12 @@ +package com.axonivy.market.exceptions; + +import static com.axonivy.market.constants.ErrorMessageConstants.INVALID_MISSING_HEADER_ERROR_MESSAGE; + +public class MissingHeaderException extends Exception { + + private static final long serialVersionUID = 1L; + + public MissingHeaderException() { + super(INVALID_MISSING_HEADER_ERROR_MESSAGE); + } +} diff --git a/src/main/java/com/axonivy/market/model/ApiError.java b/src/main/java/com/axonivy/market/model/ApiError.java new file mode 100644 index 000000000..c07b0d1a8 --- /dev/null +++ b/src/main/java/com/axonivy/market/model/ApiError.java @@ -0,0 +1,15 @@ +package com.axonivy.market.model; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class ApiError { + private HttpStatus status; + private String message; +} diff --git a/src/main/java/com/axonivy/market/repository/UserRepository.java b/src/main/java/com/axonivy/market/repository/UserRepository.java new file mode 100644 index 000000000..6a011e9c6 --- /dev/null +++ b/src/main/java/com/axonivy/market/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.User; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends MongoRepository { +} diff --git a/src/main/java/com/axonivy/market/service/UserService.java b/src/main/java/com/axonivy/market/service/UserService.java new file mode 100644 index 000000000..99c08f031 --- /dev/null +++ b/src/main/java/com/axonivy/market/service/UserService.java @@ -0,0 +1,9 @@ +package com.axonivy.market.service; + +import java.util.List; + +import com.axonivy.market.entity.User; + +public interface UserService { + List getAllUsers(); +} diff --git a/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java new file mode 100644 index 000000000..74ac69382 --- /dev/null +++ b/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java @@ -0,0 +1,24 @@ +package com.axonivy.market.service.impl; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.axonivy.market.entity.User; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.UserService; + +@Service +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public List getAllUsers() { + return userRepository.findAll(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..adda83b24 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.application.name=marketplace-service +spring.data.mongodb.host= mongodb://localhost:27017/ +spring.data.mongodb.database=marketWeb +server.port=8080 +logging.level.org.springframework.web=info +request.header=ivy \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/src/test/java/com/axonivy/market/service/UserServiceImplTest.java new file mode 100644 index 000000000..102c6f3c7 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/UserServiceImplTest.java @@ -0,0 +1,42 @@ +package com.axonivy.market.service; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.entity.User; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.UserServiceImpl; + +@ExtendWith(MockitoExtension.class) +public class UserServiceImplTest { + + @InjectMocks + private UserServiceImpl employeeService; + + @Mock + private UserRepository userRepository; + + @Test + public void testFindAllUser() { + // Mock data and service + User mockUser = new User(); + mockUser.setId("123"); + mockUser.setUsername("tvtTest"); + mockUser.setPassword("12345"); + List mockResultReturn = List.of(mockUser); + Mockito.when(userRepository.findAll()).thenReturn(mockResultReturn); + + // exercise + List result = employeeService.getAllUsers(); + + // Verify + Assertions.assertEquals(result, mockResultReturn); + } +} From 7c73ea1b5ade51a7f34cf94d706c302519fd5ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= Date: Thu, 30 May 2024 14:45:21 +0700 Subject: [PATCH 02/62] MARP-275 update readme and log level --- README.md | 5 +++-- .../com/axonivy/market/config/OpenApiConfig.java | 13 ++++++++----- src/main/resources/application.properties | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6682bcb6c..68876d06f 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ The following guides illustrate how to use some features concretely: * You can change the configuration in file “application.properties“ ### In case of using eclipse you should install manually Lombok . -* Download lombok here Download -* run command java -jar lombok.jar and restart the eclipse then you can access file “eclipse.ini“ in eclipse folder where you install → there is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are successful +* Download lombok here https://projectlombok.org/download +* run command "java -jar lombok.jar" then you can access file “eclipse.ini“ in eclipse folder where you install → there is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are successful +* Start eclipse * Import the project then in the eclipse , you should run the command “mvn clean install“ * After that you go to class MarketplaceServiceApplication → right click to main method → click run as → choose Java Application * Then you can send a request in postman :) diff --git a/src/main/java/com/axonivy/market/config/OpenApiConfig.java b/src/main/java/com/axonivy/market/config/OpenApiConfig.java index df02c02f3..3c175c1c8 100644 --- a/src/main/java/com/axonivy/market/config/OpenApiConfig.java +++ b/src/main/java/com/axonivy/market/config/OpenApiConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; @@ -16,11 +17,13 @@ public GroupedOpenApi customHeaderOpenApi() { } private OpenApiCustomizer customGlobalHeaders() { - return openApi -> openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> { - Parameter headerParameter = new Parameter().in("header").schema(new StringSchema()).name("X-Requested-By") - .description("ivy").required(true); + return openApi -> openApi.getPaths().values().forEach(pathItem -> { + for (Operation operation : pathItem.readOperations()) { + Parameter headerParameter = new Parameter().in("header").schema(new StringSchema()).name("X-Requested-By") + .description("ivy").required(true); - operation.addParametersItem(headerParameter); - })); + operation.addParametersItem(headerParameter); + } + }); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index adda83b24..05bc0242f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,5 +2,5 @@ spring.application.name=marketplace-service spring.data.mongodb.host= mongodb://localhost:27017/ spring.data.mongodb.database=marketWeb server.port=8080 -logging.level.org.springframework.web=info +logging.level.org.springframework.web=warn request.header=ivy \ No newline at end of file From e8bc03725e0e9592c5857cd57b0a908eea302577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= Date: Thu, 30 May 2024 14:52:12 +0700 Subject: [PATCH 03/62] MARP-275 add specific type for pathItem --- src/main/java/com/axonivy/market/config/OpenApiConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/axonivy/market/config/OpenApiConfig.java b/src/main/java/com/axonivy/market/config/OpenApiConfig.java index 3c175c1c8..be0f4cced 100644 --- a/src/main/java/com/axonivy/market/config/OpenApiConfig.java +++ b/src/main/java/com/axonivy/market/config/OpenApiConfig.java @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Configuration; import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; @@ -17,7 +18,7 @@ public GroupedOpenApi customHeaderOpenApi() { } private OpenApiCustomizer customGlobalHeaders() { - return openApi -> openApi.getPaths().values().forEach(pathItem -> { + return openApi -> openApi.getPaths().values().forEach((PathItem pathItem) -> { for (Operation operation : pathItem.readOperations()) { Parameter headerParameter = new Parameter().in("header").schema(new StringSchema()).name("X-Requested-By") .description("ivy").required(true); From 7c43b6bf9488a992539131fdb6a544a4c271651c Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Fri, 31 May 2024 08:58:09 +0700 Subject: [PATCH 04/62] Feature/MARP-277 setup infrastructure for our dev server (#6) * Update mongoDB * Added cros feature * Add forward * Handle feedback --- .github/workflows/ci-build.yml | 6 +++--- .github/workflows/dev-build.yml | 18 ++++++++++++++++-- .../com/axonivy/market/config/WebConfig.java | 9 +++++++++ src/main/resources/application.properties | 7 ++++--- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 04a0a85c7..4068d13e9 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -10,7 +10,7 @@ jobs: name: Sonarqube analysis runs-on: self-hosted env: - SONAR_PROJECT_KEY: "AxonIvy-Market-Service" + SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -23,8 +23,8 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Build with Maven - run: mvn clean install + - name: Build and test with Maven + run: mvn clean test - uses: sonarsource/sonarqube-scan-action@master env: SONAR_TOKEN: ${{ env.SONAR_TOKEN }} diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 63418eb2b..db32f52fe 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -19,6 +19,20 @@ jobs: java-version: '17' distribution: 'temurin' cache: maven + - name: Update MongoDB in application.properties + env: + MONGODB_HOST: ${{ secrets.MONGODB_HOST }} + MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }} + run: | + sed -i "s/^spring.data.mongodb.host=.*$/spring.data.mongodb.host=$MONGODB_HOST/" src/main/resources/application.properties + sed -i "s/^spring.data.mongodb.database=.*$/spring.data.mongodb.database=$MONGODB_DATABASE/" src/main/resources/application.properties - name: Build with Maven - run: mvn -B package --file pom.xml - + run: mvn clean package -DskipTests + - name: Prepare deployment directory + run: mkdir -p deployment && cp target/*.war deployment/ + - name: Copy WAR to Tomcat server + run: sudo cp deployment/*.war /opt/tomcat/webapps/marketplace-server.war + - name: Restart Tomcat server + run: | + sudo systemctl stop tomcat + sudo systemctl start tomcat diff --git a/src/main/java/com/axonivy/market/config/WebConfig.java b/src/main/java/com/axonivy/market/config/WebConfig.java index f26adc247..3ac974686 100644 --- a/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/src/main/java/com/axonivy/market/config/WebConfig.java @@ -3,6 +3,7 @@ import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -19,4 +20,12 @@ public WebConfig(HeaderInterceptor headerInterceptor) { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(headerInterceptor).addPathPatterns(USER_MAPPING); } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowedHeaders("*"); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 05bc0242f..bf73652c4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,7 @@ spring.application.name=marketplace-service -spring.data.mongodb.host= mongodb://localhost:27017/ -spring.data.mongodb.database=marketWeb +spring.data.mongodb.host=mongodb://localhost:27017/ +spring.data.mongodb.database=marketplace server.port=8080 logging.level.org.springframework.web=warn -request.header=ivy \ No newline at end of file +request.header=ivy +server.forward-headers-strategy=framework \ No newline at end of file From a98c65f59b28e883fe32ef5c3375be25e4c49921 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:39:01 +0700 Subject: [PATCH 05/62] MARP-277 sonar for spring boot (#12) * Add sonar profile to pom * Add sonar dependency * Fix jacoco report * Add sonar property --- .github/workflows/ci-build.yml | 59 +++-- LICENSE | 201 ++++++++++++++++++ SECURITY.md | 25 +++ pom.xml | 170 +++++++++------ sonar-project.properties | 5 + .../market/constants/DocumentConstants.java | 4 + .../constants/ErrorMessageConstants.java | 4 + .../constants/RequestMappingConstants.java | 4 + .../market/service/impl/UserServiceImpl.java | 1 + 9 files changed, 369 insertions(+), 104 deletions(-) create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 4068d13e9..86a8ff9ed 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -6,54 +6,45 @@ on: workflow_dispatch: jobs: + build: + name: Executes Tests + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Tests with Maven + run: mvn clean install analysis: name: Sonarqube analysis + needs: build runs-on: self-hosted env: - SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - + SONAR_PROJECT_KEY : ${{ secrets.SONAR_PROJECT_KEY }} steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - - name: Build and test with Maven - run: mvn clean test - - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ env.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} - with: - args: - -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} - -Dsonar.java.binaries=target/classes + - name: Run SonarQube Scanner + run: | + mvn -B verify sonar:sonar \ + -Dsonar.host.url=${{ env.SONAR_HOST_URL }} \ + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} \ + -Dsonar.projectName="AxonIvy Market Service" \ + -Dsonar.token=${{ env.SONAR_TOKEN }} \ - name: SonarQube Quality Gate check id: sonarqube-quality-gate-check uses: sonarsource/sonarqube-quality-gate-action@master timeout-minutes: 5 - env: - SONAR_TOKEN: ${{ env.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} with: - args: - -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} - build: - name: Executes Tests - runs-on: self-hosted - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - name: Tests with Maven - run: mvn -B test --file pom.xml + scanMetadataReportFile: target/sonar/report-task.txt + args: -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..1d4c06f71 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +## Reporting a Vulnerability + +At Axon Ivy, we take security seriously. If you believe you've found a security vulnerability in our software, we encourage you to let us know right away. We investigate all reported vulnerabilities promptly. + +To report a vulnerability, please send an email to [security@axonivy.com](mailto:security@axonivy.com) with the following information: + +- Description of the vulnerability +- Steps to reproduce the vulnerability +- Any additional information or context that may be helpful + +Please refrain from publicly disclosing the vulnerability until it has been addressed by our team. + +## Response Time + +We strive to respond to security vulnerability reports as quickly as possible. Upon receiving your report, we will acknowledge it within 72 hours and we will release a patch as soon as possible depending on complexity, but historically within a few days. +Please report (suspected) security vulnerabilities at https://support.axonivy.com/. + + +## Responsible Disclosure + +We encourage responsible disclosure of security vulnerabilities. We believe that working together with security researchers and the broader community helps us improve the security of our software for everyone. + +## Contact + +For any questions or concerns regarding security, please contact us at [security@axonivy.com](mailto:security@axonivy.com). diff --git a/pom.xml b/pom.xml index bf1326f7b..e35317a0e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,74 +1,104 @@ - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.2.5 - - - com.axonivy.market - marketplace-service - 0.0.1-SNAPSHOT - war - marketplace-service - marketplace-service - - 17 - - - - org.springframework.boot - spring-boot-starter-data-mongodb - - - org.springframework.boot - spring-boot-starter-web - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - org.springframework.boot - spring-boot-starter-test - test - - - org.junit.jupiter - junit-jupiter-engine - 5.9.2 - test - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.5.0 - - + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.axonivy.market + marketplace-service + 0.0.1-SNAPSHOT + war + marketplace-service + marketplace-service + + 17 + - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 4.0.0.4121 + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + verify + + report + + + + + + diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..950647ff3 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,5 @@ +sonar.sources=src/main/java +sonar.tests=src/test/java +sonar.java.binaries=target/classes +sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml +sonar.exclusions=**/test/**,**/tests/**,**/src/test/**,**/src/**/test/** \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/constants/DocumentConstants.java b/src/main/java/com/axonivy/market/constants/DocumentConstants.java index ef95831f2..be5449ceb 100644 --- a/src/main/java/com/axonivy/market/constants/DocumentConstants.java +++ b/src/main/java/com/axonivy/market/constants/DocumentConstants.java @@ -1,5 +1,9 @@ package com.axonivy.market.constants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class DocumentConstants { public static final String USER_DOCUMENT = "User"; } diff --git a/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java b/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java index 7796172fd..4ad9f831a 100644 --- a/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java +++ b/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java @@ -1,5 +1,9 @@ package com.axonivy.market.constants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class ErrorMessageConstants { public static final String INVALID_MISSING_HEADER_ERROR_MESSAGE = "Invalid or missing header"; } diff --git a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index a0f9d15ac..1293eaba4 100644 --- a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -1,5 +1,9 @@ package com.axonivy.market.constants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class RequestMappingConstants { public static final String USER_MAPPING = "/user"; } diff --git a/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java index 74ac69382..dc9990949 100644 --- a/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java +++ b/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java @@ -21,4 +21,5 @@ public UserServiceImpl(UserRepository userRepository) { public List getAllUsers() { return userRepository.findAll(); } + } From 81dbbc7085cffb6e4d0eb83ec9b77014931310f7 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:41:48 +0700 Subject: [PATCH 06/62] MARP-394 MP APIs to fetch all artifacts and search (#15) * Update dependence * Update git ignore * Use repsentation model * Update paging and message * Update common GitHub Service * Unify codes * Add OPTIONS to check * Introduce test * Fix sonar --- .gitignore | 3 + pom.xml | 10 + sonar-project.properties | 3 +- .../market/MarketplaceServiceApplication.java | 37 +++ .../assembler/ProductModelAssembler.java | 38 +++ .../config/MarketApiDocumentConfig.java | 40 +++ ...ptor.java => MarketHeaderInterceptor.java} | 11 +- .../axonivy/market/config/OpenApiConfig.java | 30 -- .../com/axonivy/market/config/WebConfig.java | 27 +- .../market/constants/CommonConstants.java | 14 + .../market/constants/DocumentConstants.java | 9 - .../market/constants/EntityConstants.java | 11 + .../market/constants/GitHubConstants.java | 11 + .../constants/RequestMappingConstants.java | 6 + .../market/controller/AppController.java | 43 +++ .../market/controller/ProductController.java | 80 +++++ .../controller/ProductDetailsController.java | 22 ++ .../axonivy/market/entity/GitHubRepoMeta.java | 20 ++ .../com/axonivy/market/entity/Product.java | 67 +++++ .../java/com/axonivy/market/entity/User.java | 4 +- .../com/axonivy/market/enums/ErrorCode.java | 26 ++ .../com/axonivy/market/enums/FileStatus.java | 25 ++ .../com/axonivy/market/enums/FileType.java | 25 ++ .../com/axonivy/market/enums/SortOption.java | 28 ++ .../com/axonivy/market/enums/TypeOption.java | 30 ++ .../market/exceptions/ExceptionHandlers.java | 28 +- .../model/InvalidParamException.java | 24 ++ .../{ => model}/MissingHeaderException.java | 2 +- .../exceptions/model/NotFoundException.java | 30 ++ .../market/factory/ProductFactory.java | 93 ++++++ .../market/github/model/GitHubFile.java | 22 ++ .../market/github/model/MavenArtifact.java | 23 ++ .../com/axonivy/market/github/model/Meta.java | 37 +++ .../service/GHAxonIvyMarketRepoService.java | 21 ++ .../service/GHAxonIvyProductRepoService.java | 14 + .../market/github/service/GitHubService.java | 22 ++ .../impl/GHAxonIvyMarketRepoServiceImpl.java | 139 +++++++++ .../impl/GHAxonIvyProductRepoServiceImpl.java | 49 ++++ .../service/impl/GitHubServiceImpl.java | 52 ++++ .../market/github/util/GitHubUtils.java | 49 ++++ .../com/axonivy/market/model/ApiError.java | 15 - .../com/axonivy/market/model/Message.java | 16 + .../axonivy/market/model/ProductModel.java | 42 +++ .../repository/GitHubRepoMetaRepository.java | 10 + .../market/repository/ProductRepository.java | 26 ++ .../market/schedulingtask/ScheduledTasks.java | 28 ++ .../market/service/ProductService.java | 12 + .../service/impl/ProductServiceImpl.java | 250 ++++++++++++++++ src/main/resources/application.properties | 6 +- src/main/resources/github.token | 1 + .../market/controller/AppControllerTest.java | 26 ++ .../controller/ProductControllerTest.java | 105 +++++++ .../ProductDetailsControllerTest.java | 23 ++ .../market/controller/UserControllerTest.java | 27 ++ .../market/factory/ProductFactoryTest.java | 50 ++++ .../market/handler/ExceptionHandlersTest.java | 57 ++++ .../GHAxonIvyMarketRepoServiceImplTest.java | 115 ++++++++ .../GHAxonIvyProductRepoServiceImplTest.java | 65 +++++ .../market/service/GitHubServiceImplTest.java | 56 ++++ .../service/ProductServiceImplTest.java | 276 ++++++++++++++++++ .../market/service/SchedulingTasksTest.java | 26 ++ .../market/service/UserServiceImplTest.java | 4 +- src/test/resources/meta.json | 24 ++ 63 files changed, 2409 insertions(+), 76 deletions(-) create mode 100644 src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java create mode 100644 src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java rename src/main/java/com/axonivy/market/config/{HeaderInterceptor.java => MarketHeaderInterceptor.java} (57%) delete mode 100644 src/main/java/com/axonivy/market/config/OpenApiConfig.java create mode 100644 src/main/java/com/axonivy/market/constants/CommonConstants.java delete mode 100644 src/main/java/com/axonivy/market/constants/DocumentConstants.java create mode 100644 src/main/java/com/axonivy/market/constants/EntityConstants.java create mode 100644 src/main/java/com/axonivy/market/constants/GitHubConstants.java create mode 100644 src/main/java/com/axonivy/market/controller/AppController.java create mode 100644 src/main/java/com/axonivy/market/controller/ProductController.java create mode 100644 src/main/java/com/axonivy/market/controller/ProductDetailsController.java create mode 100644 src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java create mode 100644 src/main/java/com/axonivy/market/entity/Product.java create mode 100644 src/main/java/com/axonivy/market/enums/ErrorCode.java create mode 100644 src/main/java/com/axonivy/market/enums/FileStatus.java create mode 100644 src/main/java/com/axonivy/market/enums/FileType.java create mode 100644 src/main/java/com/axonivy/market/enums/SortOption.java create mode 100644 src/main/java/com/axonivy/market/enums/TypeOption.java create mode 100644 src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java rename src/main/java/com/axonivy/market/exceptions/{ => model}/MissingHeaderException.java (87%) create mode 100644 src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java create mode 100644 src/main/java/com/axonivy/market/factory/ProductFactory.java create mode 100644 src/main/java/com/axonivy/market/github/model/GitHubFile.java create mode 100644 src/main/java/com/axonivy/market/github/model/MavenArtifact.java create mode 100644 src/main/java/com/axonivy/market/github/model/Meta.java create mode 100644 src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java create mode 100644 src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java create mode 100644 src/main/java/com/axonivy/market/github/service/GitHubService.java create mode 100644 src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java create mode 100644 src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java create mode 100644 src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java create mode 100644 src/main/java/com/axonivy/market/github/util/GitHubUtils.java delete mode 100644 src/main/java/com/axonivy/market/model/ApiError.java create mode 100644 src/main/java/com/axonivy/market/model/Message.java create mode 100644 src/main/java/com/axonivy/market/model/ProductModel.java create mode 100644 src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java create mode 100644 src/main/java/com/axonivy/market/repository/ProductRepository.java create mode 100644 src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java create mode 100644 src/main/java/com/axonivy/market/service/ProductService.java create mode 100644 src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java create mode 100644 src/main/resources/github.token create mode 100644 src/test/java/com/axonivy/market/controller/AppControllerTest.java create mode 100644 src/test/java/com/axonivy/market/controller/ProductControllerTest.java create mode 100644 src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java create mode 100644 src/test/java/com/axonivy/market/controller/UserControllerTest.java create mode 100644 src/test/java/com/axonivy/market/factory/ProductFactoryTest.java create mode 100644 src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java create mode 100644 src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java create mode 100644 src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java create mode 100644 src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java create mode 100644 src/test/java/com/axonivy/market/service/ProductServiceImplTest.java create mode 100644 src/test/java/com/axonivy/market/service/SchedulingTasksTest.java create mode 100644 src/test/resources/meta.json diff --git a/.gitignore b/.gitignore index 0384b84cd..3ccbd3e32 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ build/ ### VS Code ### .vscode/ /bin/ + +### Token +*.token diff --git a/pom.xml b/pom.xml index e35317a0e..947e700d7 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,16 @@ springdoc-openapi-starter-webmvc-ui 2.5.0 + + org.springframework.hateoas + spring-hateoas + 2.3.0 + + + org.kohsuke + github-api + 1.321 + diff --git a/sonar-project.properties b/sonar-project.properties index 950647ff3..6e8a7ea99 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,4 +2,5 @@ sonar.sources=src/main/java sonar.tests=src/test/java sonar.java.binaries=target/classes sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml -sonar.exclusions=**/test/**,**/tests/**,**/src/test/**,**/src/**/test/** \ No newline at end of file +sonar.exclusions=src/test/java/** +sonar.coverage.exclusions=src/test/java/**, src/main/java/**/config/**, src/main/java/**/constants/**, pom.xml, src/main/**/model/** \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java index f390c8fa3..06660037c 100644 --- a/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java +++ b/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -1,13 +1,50 @@ package com.axonivy.market; +import org.apache.commons.lang3.time.StopWatch; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import com.axonivy.market.service.ProductService; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@EnableAsync +@EnableScheduling @SpringBootApplication public class MarketplaceServiceApplication { + private ProductService productService; + + public MarketplaceServiceApplication(ProductService productService) { + this.productService = productService; + } + public static void main(String[] args) { SpringApplication.run(MarketplaceServiceApplication.class, args); } + @Async + @EventListener(ApplicationStartedEvent.class) + public void startInitializeSystem() { + syncProductData(); + } + + private void syncProductData() { + var watch = new StopWatch(); + log.warn("Synchronizing Market repo: Started synchronizing data for Axon Ivy Market repo"); + watch.start(); + if (productService.syncLatestDataFromMarketRepo()) { + log.warn("Synchronizing Market repo: Data is already up to date"); + } else { + watch.stop(); + log.warn("Synchronizing Market repo: Finished synchronizing data for Axon Ivy Market repo in [{}] milliseconds", + watch.getTime()); + } + } } diff --git a/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java new file mode 100644 index 000000000..6b36a9d07 --- /dev/null +++ b/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -0,0 +1,38 @@ +package com.axonivy.market.assembler; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import com.axonivy.market.controller.ProductDetailsController; +import com.axonivy.market.entity.Product; +import com.axonivy.market.model.ProductModel; + +@Component +public class ProductModelAssembler extends RepresentationModelAssemblerSupport { + + public ProductModelAssembler() { + super(ProductDetailsController.class, ProductModel.class); + } + + @Override + public ProductModel toModel(Product product) { + ProductModel resource = new ProductModel(); + resource.add(linkTo(methodOn(ProductDetailsController.class).findProduct(product.getId(), product.getType())) + .withSelfRel()); + return createResource(resource, product); + } + + private ProductModel createResource(ProductModel model, Product product) { + model.setId(product.getId()); + model.setName(product.getName()); + model.setShortDescription(product.getShortDescription()); + model.setType(product.getType()); + model.setTags(product.getTags()); + model.setLogoUrl(product.getLogoUrl()); + return model; + } + +} diff --git a/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java new file mode 100644 index 000000000..a92282a64 --- /dev/null +++ b/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -0,0 +1,40 @@ +package com.axonivy.market.config; + +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import static com.axonivy.market.constants.CommonConstants.*; + +@Configuration +public class MarketApiDocumentConfig { + private static final String DEFAULT_DOC_GROUP = "api"; + private static final String PATH_PATTERN = "/api/**"; + private static final String DEFAULT_PARAM = "ivy"; + private static final String HEADER_PARAM = "header"; + + @Bean + public GroupedOpenApi buildMarketCustomHeader() { + return GroupedOpenApi.builder() + .group(DEFAULT_DOC_GROUP) + .addOpenApiCustomizer(customMarketHeaders()) + .pathsToMatch(PATH_PATTERN) + .build(); + } + + private OpenApiCustomizer customMarketHeaders() { + return openApi -> openApi.getPaths().values().forEach((PathItem pathItem) -> { + for (Operation operation : pathItem.readOperations()) { + Parameter headerParameter = new Parameter().in(HEADER_PARAM) + .schema(new StringSchema()).name(REQUESTED_BY) + .description(DEFAULT_PARAM).required(true); + operation.addParametersItem(headerParameter); + } + }); + } +} diff --git a/src/main/java/com/axonivy/market/config/HeaderInterceptor.java b/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java similarity index 57% rename from src/main/java/com/axonivy/market/config/HeaderInterceptor.java rename to src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java index 56a845abb..1d35cb9cc 100644 --- a/src/main/java/com/axonivy/market/config/HeaderInterceptor.java +++ b/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java @@ -4,20 +4,25 @@ import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import com.axonivy.market.exceptions.MissingHeaderException; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import io.swagger.v3.oas.models.PathItem.HttpMethod; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @Component -public class HeaderInterceptor implements HandlerInterceptor { +public class MarketHeaderInterceptor implements HandlerInterceptor { @Value("${request.header}") private String requestHeader; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - if (!requestHeader.equals(request.getHeader("X-Requested-By"))) { + if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) { + return true; + } + if (!requestHeader.equals(request.getHeader(CommonConstants.REQUESTED_BY))) { throw new MissingHeaderException(); } return true; diff --git a/src/main/java/com/axonivy/market/config/OpenApiConfig.java b/src/main/java/com/axonivy/market/config/OpenApiConfig.java deleted file mode 100644 index be0f4cced..000000000 --- a/src/main/java/com/axonivy/market/config/OpenApiConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.axonivy.market.config; - -import org.springdoc.core.customizers.OpenApiCustomizer; -import org.springdoc.core.models.GroupedOpenApi; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.parameters.Parameter; - -@Configuration -public class OpenApiConfig { - @Bean - public GroupedOpenApi customHeaderOpenApi() { - return GroupedOpenApi.builder().group("default").addOpenApiCustomizer(customGlobalHeaders()).build(); - } - - private OpenApiCustomizer customGlobalHeaders() { - return openApi -> openApi.getPaths().values().forEach((PathItem pathItem) -> { - for (Operation operation : pathItem.readOperations()) { - Parameter headerParameter = new Parameter().in("header").schema(new StringSchema()).name("X-Requested-By") - .description("ivy").required(true); - - operation.addParametersItem(headerParameter); - } - }); - } -} diff --git a/src/main/java/com/axonivy/market/config/WebConfig.java b/src/main/java/com/axonivy/market/config/WebConfig.java index 3ac974686..b885a477d 100644 --- a/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/src/main/java/com/axonivy/market/config/WebConfig.java @@ -1,7 +1,6 @@ package com.axonivy.market.config; -import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; - +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -10,22 +9,34 @@ @Configuration public class WebConfig implements WebMvcConfigurer { - private final HeaderInterceptor headerInterceptor; + private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**" }; + private static final String[] ALLOWED_HEADERS = { "Accept-Language", "Content-Type", "Authorization", + "X-Requested-By", "x-requested-with", "X-Forwarded-Host" }; + private static final String[] ALLOWED_METHODS = { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; + + private final MarketHeaderInterceptor headerInterceptor; + + @Value("${market.cors.allowed.origin.patterns}") + private String marketCorsAllowedOriginPatterns; + + @Value("${market.cors.allowed.origin.maxAge}") + private int marketCorsAllowedOriginMaxAge; - public WebConfig(HeaderInterceptor headerInterceptor) { + public WebConfig(MarketHeaderInterceptor headerInterceptor) { this.headerInterceptor = headerInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(headerInterceptor).addPathPatterns(USER_MAPPING); + registry.addInterceptor(headerInterceptor).excludePathPatterns(EXCLUDE_PATHS); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("*") - .allowedMethods("GET", "POST", "PUT", "DELETE") - .allowedHeaders("*"); + .allowedOriginPatterns(marketCorsAllowedOriginPatterns) + .allowedMethods(ALLOWED_METHODS) + .allowedHeaders(ALLOWED_HEADERS) + .maxAge(marketCorsAllowedOriginMaxAge); } } diff --git a/src/main/java/com/axonivy/market/constants/CommonConstants.java b/src/main/java/com/axonivy/market/constants/CommonConstants.java new file mode 100644 index 000000000..d0e28028e --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/CommonConstants.java @@ -0,0 +1,14 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CommonConstants { + public static final int INITIAL_PAGE = 1; + public static final int INITIAL_PAGE_SIZE = 10; + public static final String SLASH = "/"; + public static final String REQUESTED_BY = "X-Requested-By"; + public static final String META_FILE = "meta.json"; + public static final String LOGO_FILE = "logo.png"; +} diff --git a/src/main/java/com/axonivy/market/constants/DocumentConstants.java b/src/main/java/com/axonivy/market/constants/DocumentConstants.java deleted file mode 100644 index be5449ceb..000000000 --- a/src/main/java/com/axonivy/market/constants/DocumentConstants.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.axonivy.market.constants; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class DocumentConstants { - public static final String USER_DOCUMENT = "User"; -} diff --git a/src/main/java/com/axonivy/market/constants/EntityConstants.java b/src/main/java/com/axonivy/market/constants/EntityConstants.java new file mode 100644 index 000000000..eaf213b1d --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -0,0 +1,11 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EntityConstants { + public static final String USER = "User"; + public static final String PRODUCT = "Product"; + public static final String GH_REPO_META = "GitHubRepoMeta"; +} diff --git a/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/src/main/java/com/axonivy/market/constants/GitHubConstants.java new file mode 100644 index 000000000..22ce4a3bf --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -0,0 +1,11 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GitHubConstants { + public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; + public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; + public static final String AXONIVY_MARKETPLACE_PATH = "market"; +} diff --git a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index 1293eaba4..b3c02f12c 100644 --- a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -5,5 +5,11 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RequestMappingConstants { + public static final String ROOT = "/"; + public static final String API = ROOT + "api"; + public static final String SYNC = ROOT + "sync"; public static final String USER_MAPPING = "/user"; + public static final String PRODUCT = API + "/product"; + public static final String PRODUCT_DETAILS = API + "/product-details"; + public static final String SWAGGER_URL = "/swagger-ui/index.html"; } diff --git a/src/main/java/com/axonivy/market/controller/AppController.java b/src/main/java/com/axonivy/market/controller/AppController.java new file mode 100644 index 000000000..60523b72a --- /dev/null +++ b/src/main/java/com/axonivy/market/controller/AppController.java @@ -0,0 +1,43 @@ +package com.axonivy.market.controller; + +import static com.axonivy.market.constants.RequestMappingConstants.ROOT; +import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; + +import org.springframework.http.HttpStatus; +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 org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@RestController +@RequestMapping(ROOT) +public class AppController { + + @GetMapping + public ResponseEntity root() { + var message = new Message(); + message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); + message.setMessageDetails( + "Marketplace API is a REST APIs for Marketplace website. Try with %s" + .formatted(extractSwaggerUrl())); + message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); + return new ResponseEntity<>(message, HttpStatus.OK); + } + + private String extractSwaggerUrl() { + var swaggerURL = SWAGGER_URL; + try { + swaggerURL = ServletUriComponentsBuilder.fromCurrentContextPath().path(SWAGGER_URL).toUriString(); + } catch (Exception e) { + log.error("Cannot get Swagger Url", e); + } + return swaggerURL; + } +} diff --git a/src/main/java/com/axonivy/market/controller/ProductController.java b/src/main/java/com/axonivy/market/controller/ProductController.java new file mode 100644 index 000000000..3f126c9f7 --- /dev/null +++ b/src/main/java/com/axonivy/market/controller/ProductController.java @@ -0,0 +1,80 @@ +package com.axonivy.market.controller; + +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; + +import org.apache.commons.lang3.time.StopWatch; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import com.axonivy.market.model.ProductModel; +import com.axonivy.market.service.ProductService; + +import io.swagger.v3.oas.annotations.Operation; + +@RestController +@RequestMapping(PRODUCT) +public class ProductController { + + private final ProductService service; + private final ProductModelAssembler assembler; + private final PagedResourcesAssembler pagedResourcesAssembler; + + public ProductController(ProductService service, ProductModelAssembler assembler, + PagedResourcesAssembler pagedResourcesAssembler) { + this.service = service; + this.assembler = assembler; + this.pagedResourcesAssembler = pagedResourcesAssembler; + } + + @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") + @GetMapping() + public ResponseEntity> findProducts(@RequestParam(required = true) String type, + @RequestParam(required = false) String keyword, Pageable pageable) { + Page results = service.findProducts(type, keyword, pageable); + if (results.isEmpty()) { + return generateEmptyPagedModel(); + } + var responseContent = new PageImpl(results.getContent(), pageable, results.getTotalElements()); + var pageResources = pagedResourcesAssembler.toModel(responseContent, assembler); + return new ResponseEntity<>(pageResources, HttpStatus.OK); + } + + @PutMapping(SYNC) + public ResponseEntity syncProducts() { + var stopWatch = new StopWatch(); + stopWatch.start(); + var isAlreadyUpToDate = service.syncLatestDataFromMarketRepo(); + var message = new Message(); + message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); + message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); + if (isAlreadyUpToDate) { + message.setMessageDetails("Data is already up to date, nothing to sync"); + } else { + stopWatch.stop(); + message.setMessageDetails(String.format("Finished sync data in [%s] milliseconds", stopWatch.getTime())); + } + return new ResponseEntity<>(message, HttpStatus.OK); + } + + @SuppressWarnings("unchecked") + private ResponseEntity> generateEmptyPagedModel() { + var emptyPagedModel = (PagedModel) pagedResourcesAssembler + .toEmptyModel(Page.empty(), ProductModel.class); + return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); + } +} diff --git a/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/src/main/java/com/axonivy/market/controller/ProductDetailsController.java new file mode 100644 index 000000000..a9e8f3647 --- /dev/null +++ b/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -0,0 +1,22 @@ +package com.axonivy.market.controller; + +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; + +import org.springframework.http.HttpStatus; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(PRODUCT_DETAILS) +public class ProductDetailsController { + + @GetMapping("/{id}") + public ResponseEntity findProduct(@PathVariable("id") String key, + @RequestParam(name = "type", required = false) String type) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java b/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java new file mode 100644 index 000000000..2e0770816 --- /dev/null +++ b/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java @@ -0,0 +1,20 @@ +package com.axonivy.market.entity; + +import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Document(GH_REPO_META) +public class GitHubRepoMeta { + @Id + private String repoURL; + private String repoName; + private Long lastChange; + private String lastSHA1; +} diff --git a/src/main/java/com/axonivy/market/entity/Product.java b/src/main/java/com/axonivy/market/entity/Product.java new file mode 100644 index 000000000..75cba5e35 --- /dev/null +++ b/src/main/java/com/axonivy/market/entity/Product.java @@ -0,0 +1,67 @@ +package com.axonivy.market.entity; + +import static com.axonivy.market.constants.EntityConstants.PRODUCT; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Document(PRODUCT) +public class Product implements Serializable { + + private static final long serialVersionUID = -8770801877877277258L; + @Id + private String id; + private String marketDirectory; + private String name; + private String version; + private String shortDescription; + private String logoUrl; + private Boolean listed; + private String type; + private List tags; + private String vendor; + private String vendorImage; + private String vendorUrl; + private String platformReview; + private String cost; + private String repositoryName; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private String compatibility; + private Boolean validate; + private Boolean contactUs; + private Integer installationCount; + private Date newestPublishedDate; + private String newestReleaseVersion; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/entity/User.java b/src/main/java/com/axonivy/market/entity/User.java index f6035ac00..1b88f1095 100644 --- a/src/main/java/com/axonivy/market/entity/User.java +++ b/src/main/java/com/axonivy/market/entity/User.java @@ -1,6 +1,6 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.DocumentConstants.USER_DOCUMENT; +import static com.axonivy.market.constants.EntityConstants.USER; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @@ -12,7 +12,7 @@ @Getter @Setter @NoArgsConstructor -@Document(USER_DOCUMENT) +@Document(USER) public class User { @Id private String id; diff --git a/src/main/java/com/axonivy/market/enums/ErrorCode.java b/src/main/java/com/axonivy/market/enums/ErrorCode.java new file mode 100644 index 000000000..de9d54c28 --- /dev/null +++ b/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -0,0 +1,26 @@ +package com.axonivy.market.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +/** + * @fo {@link ErrorCode} is a presentation for a system code during proceeding + * data It has format cseo - 0000 c present for controller s present for + * service e present for entity o present for other And 0000 is a successful + * code + */ + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public enum ErrorCode { + SUCCESSFUL("0000", "SUCCESSFUL"), PRODUCT_FILTER_INVALID("1101", "PRODUCT_FILTER_INVALID"), + PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"), + GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), + GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"); + + String code; + String helpText; +} diff --git a/src/main/java/com/axonivy/market/enums/FileStatus.java b/src/main/java/com/axonivy/market/enums/FileStatus.java new file mode 100644 index 000000000..d75ca9f54 --- /dev/null +++ b/src/main/java/com/axonivy/market/enums/FileStatus.java @@ -0,0 +1,25 @@ +package com.axonivy.market.enums; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.market.exceptions.model.NotFoundException; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FileStatus { + MODIFIED("modified"), ADDED("added"), REMOVED("removed"); + + private String code; + + public static FileStatus of(String code) { + for (var status : values()) { + if (StringUtils.equalsIgnoreCase(code, status.code)) { + return status; + } + } + throw new NotFoundException(ErrorCode.GH_FILE_STATUS_INVALID, "FileStatus: " + code); + } +} diff --git a/src/main/java/com/axonivy/market/enums/FileType.java b/src/main/java/com/axonivy/market/enums/FileType.java new file mode 100644 index 000000000..75bb5beb9 --- /dev/null +++ b/src/main/java/com/axonivy/market/enums/FileType.java @@ -0,0 +1,25 @@ +package com.axonivy.market.enums; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.market.exceptions.model.NotFoundException; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FileType { + META("meta.json"), LOGO("logo.png"); + + private String fileName; + + public static FileType of(String name) { + for (var type : values()) { + if (StringUtils.endsWithIgnoreCase(name, type.getFileName())) { + return type; + } + } + throw new NotFoundException(ErrorCode.GH_FILE_TYPE_INVALID, "FileType: " + name); + } +} diff --git a/src/main/java/com/axonivy/market/enums/SortOption.java b/src/main/java/com/axonivy/market/enums/SortOption.java new file mode 100644 index 000000000..308a01bd3 --- /dev/null +++ b/src/main/java/com/axonivy/market/enums/SortOption.java @@ -0,0 +1,28 @@ +package com.axonivy.market.enums; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.market.exceptions.model.InvalidParamException; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SortOption { + POPULARITY("popularity", "installationCount"), ALPHABETICALLY("alphabetically", "name"), + RECENT("recent", "newestPublishedDate"); + + private String option; + private String code; + + public static SortOption of(String option) { + option = StringUtils.isBlank(option) ? option : option.trim(); + for (var sortOption : values()) { + if (StringUtils.equalsIgnoreCase(sortOption.option, option)) { + return sortOption; + } + } + throw new InvalidParamException(ErrorCode.PRODUCT_SORT_INVALID, "SortOption: " + option); + } +} diff --git a/src/main/java/com/axonivy/market/enums/TypeOption.java b/src/main/java/com/axonivy/market/enums/TypeOption.java new file mode 100644 index 000000000..3b513ea4a --- /dev/null +++ b/src/main/java/com/axonivy/market/enums/TypeOption.java @@ -0,0 +1,30 @@ +package com.axonivy.market.enums; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.market.exceptions.model.InvalidParamException; + +import lombok.Getter; + +@Getter +public enum TypeOption { + ALL("all", ""), CONNECTORS("connectors", "connector"), UTILITIES("utilities", "util"), SOLUTIONS("solutions", "solution"), DEMOS("demos", "demo"); + + private String option; + private String code; + + private TypeOption(String option, String code) { + this.option = option; + this.code = code; + } + + public static TypeOption of(String option) { + option = StringUtils.isBlank(option) ? option : option.trim(); + for (var filter : values()) { + if (StringUtils.equalsIgnoreCase(filter.option, option)) { + return filter; + } + } + throw new InvalidParamException(ErrorCode.PRODUCT_FILTER_INVALID, "TypeOption: " + option); + } +} diff --git a/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index 7359bd18b..3dc315608 100644 --- a/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -6,14 +6,34 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import com.axonivy.market.model.ApiError; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.Message; @ControllerAdvice public class ExceptionHandlers extends ResponseEntityExceptionHandler { @ExceptionHandler(MissingHeaderException.class) - protected ResponseEntity handleMissingServletRequestParameter(MissingHeaderException missingHeaderException) { - ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, missingHeaderException.getMessage()); - return new ResponseEntity<>(apiError, apiError.getStatus()); + public ResponseEntity handleMissingServletRequestParameter(MissingHeaderException missingHeaderException) { + var errorMessage = new Message(); + errorMessage.setMessageDetails(missingHeaderException.getMessage()); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException notFoundException) { + var errorMessage = new Message(); + errorMessage.setHelpCode(notFoundException.getCode()); + errorMessage.setMessageDetails(notFoundException.getMessage()); + return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(InvalidParamException.class) + public ResponseEntity handleInvalidException(InvalidParamException invalidDataException) { + var errorMessage = new Message(); + errorMessage.setHelpCode(invalidDataException.getCode()); + errorMessage.setMessageDetails(invalidDataException.getMessage()); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); } } diff --git a/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java b/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java new file mode 100644 index 000000000..8a82188fa --- /dev/null +++ b/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java @@ -0,0 +1,24 @@ +package com.axonivy.market.exceptions.model; + +import com.axonivy.market.enums.ErrorCode; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class InvalidParamException extends NotFoundException { + private static final long serialVersionUID = 1L; + + public InvalidParamException(String code, String message) { + super(code, message); + } + + public InvalidParamException(ErrorCode errorCode) { + super(errorCode); + } + + public InvalidParamException(ErrorCode errorCode, String additionalMessage) { + super(errorCode, additionalMessage); + } +} diff --git a/src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java b/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java similarity index 87% rename from src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java rename to src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java index 7c14e8380..4b5b158c6 100644 --- a/src/main/java/com/axonivy/market/exceptions/MissingHeaderException.java +++ b/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java @@ -1,4 +1,4 @@ -package com.axonivy.market.exceptions; +package com.axonivy.market.exceptions.model; import static com.axonivy.market.constants.ErrorMessageConstants.INVALID_MISSING_HEADER_ERROR_MESSAGE; diff --git a/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java b/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java new file mode 100644 index 000000000..e1c917749 --- /dev/null +++ b/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java @@ -0,0 +1,30 @@ +package com.axonivy.market.exceptions.model; + +import com.axonivy.market.enums.ErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class NotFoundException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private static final String SEPARATOR = "-"; + + private final String code; + private final String message; + + public NotFoundException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getHelpText(); + } + + public NotFoundException(ErrorCode errorCode, String additionalMessage) { + this.code = errorCode.getCode(); + this.message = errorCode.getHelpText() + SEPARATOR + additionalMessage; + } + +} diff --git a/src/main/java/com/axonivy/market/factory/ProductFactory.java b/src/main/java/com/axonivy/market/factory/ProductFactory.java new file mode 100644 index 000000000..da55fbf09 --- /dev/null +++ b/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -0,0 +1,93 @@ +package com.axonivy.market.factory; + +import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.CommonConstants.META_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.github.GHContent; + +import com.axonivy.market.entity.Product; +import com.axonivy.market.github.model.Meta; +import com.axonivy.market.github.util.GitHubUtils; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ProductFactory { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static Product mappingByGHContent(Product product, GHContent content) { + if (content == null) { + return product; + } + + var contentName = content.getName(); + if (StringUtils.endsWith(contentName, META_FILE)) { + mappingByMetaJSONFile(product, content); + } + if (StringUtils.endsWith(contentName, LOGO_FILE)) { + product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); + } + return product; + } + + public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { + Meta meta = null; + try { + meta = jsonDecode(ghContent); + } catch (Exception e) { + log.error("Mapping from Meta file by GHContent failed", e); + return product; + } + + product.setId(meta.getId()); + product.setName(meta.getName()); + product.setMarketDirectory(extractParentDirectory(ghContent)); + product.setListed(meta.getListed()); + product.setType(meta.getType()); + product.setTags(meta.getTags()); + product.setVersion(meta.getVersion()); + product.setShortDescription(meta.getDescription()); + product.setVendor(meta.getVendor()); + product.setVendorImage(meta.getVendorImage()); + product.setVendorUrl(meta.getVendorUrl()); + product.setPlatformReview(meta.getPlatformReview()); + product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); + product.setLanguage(meta.getLanguage()); + product.setIndustry(meta.getIndustry()); + extractSourceUrl(product, meta); + return product; + } + + private static String extractParentDirectory(GHContent ghContent) { + var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); + return path.replace(ghContent.getName(), EMPTY); + } + + private static void extractSourceUrl(Product product, Meta meta) { + var sourceUrl = meta.getSourceUrl(); + if (StringUtils.isBlank(sourceUrl)) { + return; + } + String[] tokens = sourceUrl.split(SLASH); + var tokensLength = tokens.length; + var repositoryPath = sourceUrl; + if (tokensLength > 1) { + repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); + } + product.setRepositoryName(repositoryPath); + product.setSourceUrl(sourceUrl); + } + + private static Meta jsonDecode(GHContent ghContent) throws IOException { + return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); + } +} diff --git a/src/main/java/com/axonivy/market/github/model/GitHubFile.java b/src/main/java/com/axonivy/market/github/model/GitHubFile.java new file mode 100644 index 000000000..9586f0886 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/model/GitHubFile.java @@ -0,0 +1,22 @@ +package com.axonivy.market.github.model; + +import java.util.Date; + +import com.axonivy.market.enums.FileStatus; +import com.axonivy.market.enums.FileType; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class GitHubFile { + private String fileName; + private String previousFilename; + private String path; + private FileType type; + private FileStatus status; + private Date commitDate; +} diff --git a/src/main/java/com/axonivy/market/github/model/MavenArtifact.java b/src/main/java/com/axonivy/market/github/model/MavenArtifact.java new file mode 100644 index 000000000..14af356b8 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/model/MavenArtifact.java @@ -0,0 +1,23 @@ +package com.axonivy.market.github.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MavenArtifact { + private String repoUrl; + private String name; + private String groupId; + private String artifactId; + private String type; +} diff --git a/src/main/java/com/axonivy/market/github/model/Meta.java b/src/main/java/com/axonivy/market/github/model/Meta.java new file mode 100644 index 000000000..4ef988820 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/model/Meta.java @@ -0,0 +1,37 @@ +package com.axonivy.market.github.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Meta { + @JsonProperty("$schema") + private String schema; + private String id; + private String name; + private String description; + private String type; + private String platformReview; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private Boolean listed; + private String version; + private String vendor; + private String vendorImage; + private String vendorUrl; + private List tags; + private List mavenArtifacts; +} diff --git a/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java b/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java new file mode 100644 index 000000000..a669fac2a --- /dev/null +++ b/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java @@ -0,0 +1,21 @@ +package com.axonivy.market.github.service; + +import java.util.List; +import java.util.Map; + +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; + +import com.axonivy.market.github.model.GitHubFile; + +public interface GHAxonIvyMarketRepoService { + + public Map> fetchAllMarketItems(); + + public GHCommit getLastCommit(long lastCommitTime); + + public List fetchMarketItemsBySHA1Range(String fromSHA1, String toSHA1); + + public GHRepository getRepository(); +} diff --git a/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java new file mode 100644 index 000000000..1665799de --- /dev/null +++ b/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -0,0 +1,14 @@ +package com.axonivy.market.github.service; + +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHTag; + +import java.io.IOException; +import java.util.List; + +public interface GHAxonIvyProductRepoService { + + GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion); + + List getAllTagsFromRepoName(String repoName) throws IOException; +} diff --git a/src/main/java/com/axonivy/market/github/service/GitHubService.java b/src/main/java/com/axonivy/market/github/service/GitHubService.java new file mode 100644 index 000000000..2f4fb7e16 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -0,0 +1,22 @@ +package com.axonivy.market.github.service; + +import java.io.IOException; +import java.util.List; + +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; + +public interface GitHubService { + + public GitHub getGitHub() throws IOException; + + public GHOrganization getOrganization(String orgName) throws IOException; + + public GHRepository getRepository(String repositoryPath) throws IOException; + + public List getDirectoryContent(GHRepository ghRepository, String path) throws IOException; + + public GHContent getGHContent(GHRepository ghRepository, String path) throws IOException; +} diff --git a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java new file mode 100644 index 000000000..0d5202c18 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -0,0 +1,139 @@ +package com.axonivy.market.github.service.impl; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHCommitQueryBuilder; +import org.kohsuke.github.GHCompare; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.springframework.stereotype.Service; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.enums.FileStatus; +import com.axonivy.market.enums.FileType; +import com.axonivy.market.github.model.GitHubFile; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.util.GitHubUtils; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Service +public class GHAxonIvyMarketRepoServiceImpl implements GHAxonIvyMarketRepoService { + private static final String DEFAULT_BRANCH = "master"; + private static final LocalDateTime INITIAL_COMMIT_DATE = LocalDateTime.of(2020, 10, 30, 0, 0); + private GHOrganization organization; + private GHRepository repository; + + private final GitHubService gitHubService; + + public GHAxonIvyMarketRepoServiceImpl(GitHubService gitHubService) { + this.gitHubService = gitHubService; + } + + @Override + public Map> fetchAllMarketItems() { + Map> ghContentMap = new HashMap<>(); + try { + List directoryContent = gitHubService.getDirectoryContent(getRepository(), + GitHubConstants.AXONIVY_MARKETPLACE_PATH); + for (var content : directoryContent) { + extractFileInDirectoryContent(content, ghContentMap); + } + } catch (Exception e) { + log.error("Cannot fetch GHContent: ", e); + } + return ghContentMap; + } + + private void extractFileInDirectoryContent(GHContent content, Map> ghContentMap) + throws IOException { + if (content != null && content.isDirectory()) { + for (var childContent : GitHubUtils.mapPagedIteratorToList(content.listDirectoryContent())) { + if (childContent.isFile()) { + var contents = ghContentMap.getOrDefault(content.getPath(), new ArrayList<>()); + contents.add(childContent); + ghContentMap.putIfAbsent(content.getPath(), contents); + } else { + extractFileInDirectoryContent(childContent, ghContentMap); + } + } + } + } + + @Override + public GHCommit getLastCommit(long lastCommitTime) { + if (lastCommitTime == 0l) { + lastCommitTime = INITIAL_COMMIT_DATE.atZone(ZoneId.systemDefault()).toEpochSecond(); + } + try { + GHCommitQueryBuilder commitBuilder = createQueryCommitsBuilder(lastCommitTime); + return GitHubUtils.mapPagedIteratorToList(commitBuilder.list()).stream().findFirst().orElse(null); + } catch (Exception e) { + log.error("Cannot query GHCommit: ", e); + } + return null; + } + + private GHCommitQueryBuilder createQueryCommitsBuilder(long lastCommitTime) { + return getRepository().queryCommits().since(lastCommitTime).from(DEFAULT_BRANCH); + } + + @Override + public List fetchMarketItemsBySHA1Range(String fromSHA1, String toSHA1) { + Map gitHubFileMap = new HashMap<>(); + try { + GHCompare compareResult = getRepository().getCompare(fromSHA1, toSHA1); + for (var commit : GitHubUtils.mapPagedIteratorToList(compareResult.listCommits())) { + var listFiles = commit.listFiles(); + if (listFiles == null) { + continue; + } + GitHubUtils.mapPagedIteratorToList(listFiles).forEach(file -> { + String fullPathName = file.getFileName(); + if (FileType.of(fullPathName) != null) { + var gitHubFile = new GitHubFile(); + gitHubFile.setFileName(fullPathName); + gitHubFile.setPath(file.getRawUrl().getPath()); + gitHubFile.setStatus(FileStatus.of(file.getStatus())); + gitHubFile.setType(FileType.of(fullPathName)); + gitHubFile.setPreviousFilename(file.getPreviousFilename()); + gitHubFileMap.put(fullPathName, gitHubFile); + } + }); + } + } catch (Exception e) { + log.error("Cannot get GH compare: ", e); + } + return new ArrayList<>(gitHubFileMap.values()); + } + + private GHOrganization getOrganization() throws IOException { + if (organization == null) { + organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + } + return organization; + } + + @Override + public GHRepository getRepository() { + if (repository == null) { + try { + repository = getOrganization().getRepository(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); + } catch (IOException e) { + log.error("Get AxonIvy Market repo failed: ", e); + } + } + return repository; + } + +} diff --git a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java new file mode 100644 index 000000000..def4bd9b4 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -0,0 +1,49 @@ +package com.axonivy.market.github.service.impl; + +import java.io.IOException; +import java.util.List; + +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHTag; +import org.springframework.stereotype.Service; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.service.GitHubService; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Service +public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { + private GHOrganization organization; + + private final GitHubService gitHubService; + + public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { + this.gitHubService = gitHubService; + } + + @Override + public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { + try { + return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); + } catch (IOException e) { + log.error("Cannot Get Content From File Directory", e); + return null; + } + } + + private GHOrganization getOrganization() throws IOException { + if (organization == null) { + organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + } + return organization; + } + + @Override + public List getAllTagsFromRepoName(String repoName) throws IOException { + return getOrganization().getRepository(repoName).listTags().toList(); + } +} diff --git a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java new file mode 100644 index 000000000..a5bea0728 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -0,0 +1,52 @@ +package com.axonivy.market.github.service.impl; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; + +import com.axonivy.market.github.service.GitHubService; + +@Service +public class GitHubServiceImpl implements GitHubService { + + private static final String GITHUB_TOKEN_FILE = "classpath:github.token"; + + @Override + public GitHub getGitHub() throws IOException { + File gitHubToken = ResourceUtils.getFile(GITHUB_TOKEN_FILE); + var token = Files.readString(gitHubToken.toPath()); + return new GitHubBuilder().withOAuthToken(token).build(); + } + + @Override + public GHOrganization getOrganization(String orgName) throws IOException { + return getGitHub().getOrganization(orgName); + } + + @Override + public List getDirectoryContent(GHRepository ghRepository, String path) throws IOException { + Assert.notNull(ghRepository, "Repository must not be null"); + return ghRepository.getDirectoryContent(path); + } + + @Override + public GHRepository getRepository(String repositoryPath) throws IOException { + return getGitHub().getRepository(repositoryPath); + } + + @Override + public GHContent getGHContent(GHRepository ghRepository, String path) throws IOException { + Assert.notNull(ghRepository, "Repository must not be null"); + return ghRepository.getFileContent(path); + } +} diff --git a/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/src/main/java/com/axonivy/market/github/util/GitHubUtils.java new file mode 100644 index 000000000..8d66b4649 --- /dev/null +++ b/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -0,0 +1,49 @@ +package com.axonivy.market.github.util; + +import java.io.IOException; +import java.util.List; + +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.PagedIterable; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GitHubUtils { + + public static long getGHCommitDate(GHCommit commit) { + long commitTime = 0l; + if (commit != null) { + try { + commitTime = commit.getCommitDate().getTime(); + } catch (Exception e) { + log.error("Check last commit failed", e); + } + } + return commitTime; + } + + public static String getDownloadUrl(GHContent content) { + try { + return content.getDownloadUrl(); + } catch (IOException e) { + log.error("Cannot get DownloadURl from GHContent: ", e); + } + return ""; + } + + public static List mapPagedIteratorToList(PagedIterable paged) { + if (paged != null) { + try { + return paged.toList(); + } catch (IOException e) { + log.error("Cannot parse to list for pagediterable: ", e); + } + } + return List.of(); + } +} diff --git a/src/main/java/com/axonivy/market/model/ApiError.java b/src/main/java/com/axonivy/market/model/ApiError.java deleted file mode 100644 index c07b0d1a8..000000000 --- a/src/main/java/com/axonivy/market/model/ApiError.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.axonivy.market.model; - -import org.springframework.http.HttpStatus; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@AllArgsConstructor -public class ApiError { - private HttpStatus status; - private String message; -} diff --git a/src/main/java/com/axonivy/market/model/Message.java b/src/main/java/com/axonivy/market/model/Message.java new file mode 100644 index 000000000..45754acaf --- /dev/null +++ b/src/main/java/com/axonivy/market/model/Message.java @@ -0,0 +1,16 @@ +package com.axonivy.market.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Message { + private String helpCode; + private String helpText; + private String messageDetails; +} diff --git a/src/main/java/com/axonivy/market/model/ProductModel.java b/src/main/java/com/axonivy/market/model/ProductModel.java new file mode 100644 index 000000000..e088f1230 --- /dev/null +++ b/src/main/java/com/axonivy/market/model/ProductModel.java @@ -0,0 +1,42 @@ +package com.axonivy.market.model; + +import java.util.List; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@Relation(collectionRelation = "products", itemRelation = "product") +@JsonInclude(Include.NON_NULL) +public class ProductModel extends RepresentationModel { + private String id; + private String name; + private String shortDescription; + private String logoUrl; + private String type; + private List tags; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((ProductModel) obj).getId()).isEquals(); + } +} diff --git a/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java b/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java new file mode 100644 index 000000000..49424d63c --- /dev/null +++ b/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java @@ -0,0 +1,10 @@ +package com.axonivy.market.repository; + +import org.springframework.data.mongodb.repository.MongoRepository; + +import com.axonivy.market.entity.GitHubRepoMeta; + +public interface GitHubRepoMetaRepository extends MongoRepository { + + GitHubRepoMeta findByRepoName(String repoName); +} diff --git a/src/main/java/com/axonivy/market/repository/ProductRepository.java b/src/main/java/com/axonivy/market/repository/ProductRepository.java new file mode 100644 index 000000000..238cd774f --- /dev/null +++ b/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -0,0 +1,26 @@ +package com.axonivy.market.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import com.axonivy.market.entity.Product; + +@Repository +public interface ProductRepository extends MongoRepository { + + Page findByType(String type, Pageable pageable); + + Product findByLogoUrl(String logoUrl); + + @Query("{'marketDirectory': {$regex : ?0, $options: 'i'}}") + Product findByMarketDirectoryRegex(String search); + + @Query("{ $and: [ { $or: [ { 'name': { $regex: ?0, $options: 'i' } }, { 'shortDescription': { $regex: ?0, $options: 'i' } } ] }, { 'type': ?1 } ] }") + Page searchByKeywordAndType(String keyword, String type, Pageable unifiedPageabe); + + @Query("{ $or: [ { 'name': { $regex: ?0, $options: 'i' } }, { 'shortDescription': { $regex: ?0, $options: 'i' } } ] }") + Page searchByNameOrShortDescriptionRegex(String keyword, Pageable unifiedPageabe); +} diff --git a/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java new file mode 100644 index 000000000..96153ba39 --- /dev/null +++ b/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java @@ -0,0 +1,28 @@ +package com.axonivy.market.schedulingtask; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.axonivy.market.service.ProductService; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Component +public class ScheduledTasks { + + private static final String SCHEDULING_TASK_PRODUCTS_CRON = "0 0 0/1 ? * *"; + + private ProductService productService; + + public ScheduledTasks(ProductService productService) { + this.productService = productService; + } + + @Scheduled(cron = SCHEDULING_TASK_PRODUCTS_CRON) + public void syncDataForProductFromGitHubRepo() { + log.warn("Started sync data for product from GitHub repo"); + productService.syncLatestDataFromMarketRepo(); + } + +} diff --git a/src/main/java/com/axonivy/market/service/ProductService.java b/src/main/java/com/axonivy/market/service/ProductService.java new file mode 100644 index 000000000..0a3f5529f --- /dev/null +++ b/src/main/java/com/axonivy/market/service/ProductService.java @@ -0,0 +1,12 @@ +package com.axonivy.market.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.axonivy.market.entity.Product; + +public interface ProductService { + Page findProducts(String type, String keyword, Pageable pageable); + + boolean syncLatestDataFromMarketRepo(); +} diff --git a/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java new file mode 100644 index 000000000..44de63b60 --- /dev/null +++ b/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -0,0 +1,250 @@ +package com.axonivy.market.service.impl; + +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.FileType; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.factory.ProductFactory; +import com.axonivy.market.github.model.GitHubFile; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.repository.GitHubRepoMetaRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.ProductService; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Service +public class ProductServiceImpl implements ProductService { + + private final ProductRepository productRepository; + private final GHAxonIvyMarketRepoService axonIvyMarketRepoService; + private final GitHubRepoMetaRepository gitHubRepoMetaRepository; + private final GitHubService gitHubService; + + private GHCommit lastGHCommit; + private GitHubRepoMeta marketRepoMeta; + + public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, + GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService) { + this.productRepository = productRepository; + this.axonIvyMarketRepoService = axonIvyMarketRepoService; + this.gitHubRepoMetaRepository = gitHubRepoMetaRepository; + this.gitHubService = gitHubService; + } + + @Override + public Page findProducts(String type, String keyword, Pageable pageable) { + final var typeOption = TypeOption.of(type); + final var searchPageable = refinePagination(pageable); + Page result = Page.empty(); + switch (typeOption) { + case ALL: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findAll(searchPageable); + } else { + result = productRepository.searchByNameOrShortDescriptionRegex(keyword, searchPageable); + } + break; + case CONNECTORS, UTILITIES, SOLUTIONS: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findByType(typeOption.getCode(), searchPageable); + } else { + result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), searchPageable); + } + break; + default: + break; + } + return result; + } + + @Override + public boolean syncLatestDataFromMarketRepo() { + var isAlreadyUpToDate = isLastGithubCommitCovered(); + if (!isAlreadyUpToDate) { + if (marketRepoMeta == null) { + syncProductsFromGitHubRepo(); + marketRepoMeta = new GitHubRepoMeta(); + } else { + updateLatestChangeToProductsFromGithubRepo(); + } + syncRepoMetaDataStatus(); + } + return isAlreadyUpToDate; + } + + private void syncRepoMetaDataStatus() { + if (lastGHCommit == null) { + return; + } + String repoURL = Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath) + .orElse(EMPTY); + marketRepoMeta.setRepoURL(repoURL); + marketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); + marketRepoMeta.setLastSHA1(lastGHCommit.getSHA1()); + marketRepoMeta.setLastChange(GitHubUtils.getGHCommitDate(lastGHCommit)); + gitHubRepoMetaRepository.save(marketRepoMeta); + marketRepoMeta = null; + } + + private void updateLatestChangeToProductsFromGithubRepo() { + var fromSHA1 = marketRepoMeta.getLastSHA1(); + var toSHA1 = ofNullable(lastGHCommit).map(GHCommit::getSHA1).orElse(""); + log.warn("**ProductService: synchronize products from SHA1 {} to SHA1 {}", fromSHA1, toSHA1); + List gitHubFileChanges = axonIvyMarketRepoService.fetchMarketItemsBySHA1Range(fromSHA1, toSHA1); + Map> groupGitHubFiles = new HashMap<>(); + for (var file : gitHubFileChanges) { + String filePath = file.getFileName(); + var parentPath = filePath.replace(FileType.META.getFileName(), EMPTY).replace(FileType.LOGO.getFileName(), EMPTY); + var files = groupGitHubFiles.getOrDefault(parentPath, new ArrayList<>()); + files.add(file); + groupGitHubFiles.putIfAbsent(parentPath, files); + } + + groupGitHubFiles.entrySet().forEach(ghFileEntity -> { + for (var file : ghFileEntity.getValue()) { + Product product = new Product(); + GHContent fileContent; + try { + fileContent = gitHubService.getGHContent(axonIvyMarketRepoService.getRepository(), file.getFileName()); + } catch (IOException e) { + log.error("Get GHContent failed: ", e); + continue; + } + + ProductFactory.mappingByGHContent(product, fileContent); + updateLatestReleaseDateForProduct(product); + if (FileType.META == file.getType()) { + modifyProductByMetaContent(file, product); + } else { + modifyProductLogo(ghFileEntity.getKey(), file, product, fileContent); + } + } + }); + } + + private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { + Product result = null; + switch (file.getStatus()) { + case MODIFIED, ADDED: + result = productRepository.findByMarketDirectoryRegex(parentPath); + if (result != null) { + result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); + productRepository.save(result); + } + break; + case REMOVED: + result = productRepository.findByLogoUrl(product.getLogoUrl()); + if (result != null) { + productRepository.deleteById(result.getId()); + } + break; + default: + break; + } + } + + private void modifyProductByMetaContent(GitHubFile file, Product product) { + switch (file.getStatus()) { + case MODIFIED, ADDED: + productRepository.save(product); + break; + case REMOVED: + productRepository.deleteById(product.getId()); + break; + default: + break; + } + } + + private Pageable refinePagination(Pageable pageable) { + PageRequest pageRequest = (PageRequest) pageable; + if (pageable != null && pageable.getSort() != null) { + List orders = new ArrayList<>(); + for (var sort : pageable.getSort()) { + final var sortOption = SortOption.of(sort.getProperty()); + Order order = new Order(sort.getDirection(), sortOption.getCode()); + orders.add(order); + } + pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders)); + } + return pageRequest; + } + + private boolean isLastGithubCommitCovered() { + boolean isLastCommitCovered = false; + long lastCommitTime = 0l; + marketRepoMeta = gitHubRepoMetaRepository.findByRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); + if (marketRepoMeta != null) { + lastCommitTime = marketRepoMeta.getLastChange(); + } + lastGHCommit = axonIvyMarketRepoService.getLastCommit(lastCommitTime); + if (lastGHCommit != null && marketRepoMeta != null + && StringUtils.equals(lastGHCommit.getSHA1(), marketRepoMeta.getLastSHA1())) { + isLastCommitCovered = true; + } + return isLastCommitCovered; + } + + private Page syncProductsFromGitHubRepo() { + log.warn("**ProductService: synchronize products from scratch based on the Market repo"); + var gitHubContentMap = axonIvyMarketRepoService.fetchAllMarketItems(); + List products = new ArrayList<>(); + gitHubContentMap.entrySet().forEach(ghContentEntity -> { + Product product = new Product(); + for (var content : ghContentEntity.getValue()) { + ProductFactory.mappingByGHContent(product, content); + updateLatestReleaseDateForProduct(product); + } + products.add(product); + }); + if (!products.isEmpty()) { + productRepository.saveAll(products); + } + return new PageImpl<>(products); + } + + private void updateLatestReleaseDateForProduct(Product product) { + if (StringUtils.isBlank(product.getRepositoryName())) { + return; + } + try { + GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); + GHTag lastTag = CollectionUtils.firstElement(productRepo.listTags().toList()); + product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); + product.setNewestReleaseVersion(lastTag.getName()); + } catch (Exception e) { + log.error("Cannot find repository by path {} {}", product.getRepositoryName(), e); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bf73652c4..458102046 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,4 +4,8 @@ spring.data.mongodb.database=marketplace server.port=8080 logging.level.org.springframework.web=warn request.header=ivy -server.forward-headers-strategy=framework \ No newline at end of file +server.forward-headers-strategy=framework +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] +market.cors.allowed.origin.maxAge=3600 diff --git a/src/main/resources/github.token b/src/main/resources/github.token new file mode 100644 index 000000000..f0208d200 --- /dev/null +++ b/src/main/resources/github.token @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/controller/AppControllerTest.java b/src/test/java/com/axonivy/market/controller/AppControllerTest.java new file mode 100644 index 000000000..b46637a6e --- /dev/null +++ b/src/test/java/com/axonivy/market/controller/AppControllerTest.java @@ -0,0 +1,26 @@ +package com.axonivy.market.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +class AppControllerTest { + + @InjectMocks + private AppController appController; + + @Test + void testRoot() throws Exception { + var response = appController.root(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertTrue(response.getBody().getMessageDetails().contains("/swagger-ui/index.html")); + } + +} diff --git a/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/src/test/java/com/axonivy/market/controller/ProductControllerTest.java new file mode 100644 index 000000000..68e846755 --- /dev/null +++ b/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -0,0 +1,105 @@ +package com.axonivy.market.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.hateoas.PagedModel.PageMetadata; +import org.springframework.http.HttpStatus; + +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.service.ProductService; + +@ExtendWith(MockitoExtension.class) +class ProductControllerTest { + private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; + private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; + + @Mock + private ProductService service; + + @Mock + private ProductModelAssembler assembler; + + @Mock + private PagedResourcesAssembler pagedResourcesAssembler; + + @Mock + private PagedModel pagedProductModel; + + @InjectMocks + private ProductController productController; + + @BeforeEach + void setup() { + assembler = new ProductModelAssembler(); + } + + @Test + void testFindProductsAsEmpty() { + PageRequest pageable = PageRequest.of(0, 20); + Page mockProducts = new PageImpl(List.of(), pageable, 0); + when(service.findProducts(any(), any(), any())).thenReturn(mockProducts); + when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); + var result = productController.findProducts(TypeOption.ALL.getOption(), null, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(0, result.getBody().getContent().size()); + } + + @Test + void testFindProducts() { + PageRequest pageable = PageRequest.of(0, 20, Sort.by(Order.by(SortOption.ALPHABETICALLY.getOption()))); + Product mockProduct = createProductMock(); + + Page mockProducts = new PageImpl(List.of(mockProduct), pageable, 1); + when(service.findProducts(any(), any(), any())).thenReturn(mockProducts); + assembler = new ProductModelAssembler(); + var mockProductModel = assembler.toModel(mockProduct); + var mockPagedModel = PagedModel.of(List.of(mockProductModel), new PageMetadata(1, 0, 1)); + when(pagedResourcesAssembler.toModel(any(), any(ProductModelAssembler.class))).thenReturn(mockPagedModel); + var result = productController.findProducts(TypeOption.ALL.getOption(), null, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(1, result.getBody().getContent().size()); + assertEquals(PRODUCT_NAME_SAMPLE, result.getBody().getContent().iterator().next().getName()); + } + + @Test + void testSyncProducts() { + var response = productController.syncProducts(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals(ErrorCode.SUCCESSFUL.getCode(), response.getBody().getHelpCode()); + } + + private Product createProductMock() { + Product mockProduct = new Product(); + mockProduct.setId("amazon-comprehend"); + mockProduct.setName(PRODUCT_NAME_SAMPLE); + mockProduct.setShortDescription(PRODUCT_DESC_SAMPLE); + mockProduct.setType("connector"); + mockProduct.setTags(List.of("AI")); + return mockProduct; + } +} diff --git a/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java new file mode 100644 index 000000000..b8babd511 --- /dev/null +++ b/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -0,0 +1,23 @@ +package com.axonivy.market.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +@ExtendWith(MockitoExtension.class) +class ProductDetailsControllerTest { + + @InjectMocks + private ProductDetailsController productDetailsController; + + @Test + void testFindProduct() { + var result = productDetailsController.findProduct("", ""); + assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + } + +} diff --git a/src/test/java/com/axonivy/market/controller/UserControllerTest.java b/src/test/java/com/axonivy/market/controller/UserControllerTest.java new file mode 100644 index 000000000..5886b6473 --- /dev/null +++ b/src/test/java/com/axonivy/market/controller/UserControllerTest.java @@ -0,0 +1,27 @@ +package com.axonivy.market.controller; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.service.UserService; + +@ExtendWith(MockitoExtension.class) +class UserControllerTest { + + @Mock + UserService userService; + + @InjectMocks + UserController userController; + + @Test + void testGetAllUser() { + var result = userController.getAllUser(); + assertNotEquals(null, result); + } +} diff --git a/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java new file mode 100644 index 000000000..7c8099b91 --- /dev/null +++ b/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -0,0 +1,50 @@ +package com.axonivy.market.factory; + +import static com.axonivy.market.constants.CommonConstants.META_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.entity.Product; + +@ExtendWith(MockitoExtension.class) +class ProductFactoryTest { + private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; + + @Test + void testMappingByGHContent() throws IOException { + Product product = new Product(); + GHContent mockContent = mock(GHContent.class); + when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); + InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); + when(mockContent.read()).thenReturn(inputStream); + var result = ProductFactory.mappingByGHContent(product, mockContent); + assertNotEquals(null, result); + assertEquals("Amazon Comprehend", result.getName()); + } + + @Test + void testMappingLogo() throws IOException { + Product product = new Product(); + GHContent content = mock(GHContent.class); + when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + var result = ProductFactory.mappingByGHContent(product, content); + assertNotEquals(null, result); + + when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + when(content.getDownloadUrl()).thenReturn(DUMMY_LOGO_URL); + result = ProductFactory.mappingByGHContent(product, content); + assertNotEquals(null, result); + } +} diff --git a/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java new file mode 100644 index 000000000..3bbe6f80b --- /dev/null +++ b/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java @@ -0,0 +1,57 @@ +package com.axonivy.market.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +import com.axonivy.market.exceptions.ExceptionHandlers; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.Message; + +@ExtendWith(MockitoExtension.class) +class ExceptionHandlersTest { + + @InjectMocks + private ExceptionHandlers exceptionHandlers; + + @BeforeEach + public void setUp() { + exceptionHandlers = new ExceptionHandlers(); + } + + @Test + void testHandleMissingServletRequestParameter() { + var errorMessageText = "Missing header"; + var missingHeaderException = mock(MissingHeaderException.class); + when(missingHeaderException.getMessage()).thenReturn(errorMessageText); + + var responseEntity = exceptionHandlers.handleMissingServletRequestParameter(missingHeaderException); + + assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); + Message errorMessage = (Message) responseEntity.getBody(); + assertEquals(errorMessageText, errorMessage.getMessageDetails()); + } + + @Test + void testHandleNotFoundException() { + var notFoundException = mock(NotFoundException.class); + var responseEntity = exceptionHandlers.handleNotFoundException(notFoundException); + assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); + } + + @Test + void testHandleInvalidException() { + var invalidParamException = mock(InvalidParamException.class); + var responseEntity = exceptionHandlers.handleInvalidException(invalidParamException); + assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); + } +} diff --git a/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java new file mode 100644 index 000000000..d8b06b074 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -0,0 +1,115 @@ +package com.axonivy.market.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHCommit.File; +import org.kohsuke.github.GHCompare; +import org.kohsuke.github.GHCompare.Commit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.PagedIterable; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; + +@ExtendWith(MockitoExtension.class) +class GHAxonIvyMarketRepoServiceImplTest { + + @Mock + GHOrganization ghOrganization; + + @Mock + GHRepository ghRepository; + + @Mock + PagedIterable pagedGHContent; + + @Mock + PagedIterable pagedCommit; + + @Mock + PagedIterable pagedFile; + + @Mock + GitHubService gitHubService; + + @InjectMocks + GHAxonIvyMarketRepoServiceImpl axonIvyMarketRepoServiceImpl; + + @BeforeEach + void setup() throws IOException { + when(ghOrganization.getRepository(any())).thenReturn(ghRepository); + when(gitHubService.getOrganization(anyString())).thenReturn(ghOrganization); + } + + @Test + void testFetchAllMarketItems() throws IOException { + // Empty due to missing token + var ghContentMap = axonIvyMarketRepoServiceImpl.fetchAllMarketItems(); + assertEquals(0, ghContentMap.values().size()); + + // Has one record from Github-repo + var mockGHFileContent = mock(GHContent.class); + var mockGHContent = mock(GHContent.class); + when(mockGHContent.isDirectory()).thenReturn(true); + when(mockGHContent.listDirectoryContent()).thenReturn(pagedGHContent); + List mockGhContents = new ArrayList<>(); + mockGhContents.add(mockGHContent); + when(mockGHFileContent.isFile()).thenReturn(true); + when(pagedGHContent.toList()).thenReturn(List.of(mockGHFileContent)); + when(gitHubService.getDirectoryContent(any(), any())).thenReturn(mockGhContents); + + ghContentMap = axonIvyMarketRepoServiceImpl.fetchAllMarketItems(); + assertEquals(1, ghContentMap.values().size()); + } + + @Test + void testFetchMarketItemsBySHA1Range() throws IOException { + final String startSHA1 = "2f415c725b049655c6c100448b8aeed59514023b"; + final String endSHA1 = "c57259288e208feea7e18fdb2fd483081bb69fb4"; + final String fileName = "test-meta.json"; + + var mockCommit = mock(Commit.class); + var mockGHCompare = mock(GHCompare.class); + when(mockGHCompare.listCommits()).thenReturn(pagedCommit); + when(pagedCommit.toList()).thenReturn(List.of(mockCommit)); + when(ghRepository.getCompare(anyString(), anyString())).thenReturn(mockGHCompare); + + var gitHubFiles = axonIvyMarketRepoServiceImpl.fetchMarketItemsBySHA1Range(startSHA1, endSHA1); + assertEquals(0, gitHubFiles.size()); + + when(mockCommit.listFiles()).thenReturn(pagedFile); + var mockFile = mock(File.class); + when(mockFile.getFileName()).thenReturn(fileName); + when(mockFile.getRawUrl()).thenReturn(new URL("http://github/test-repo-url/test-meta.json")); + when(mockFile.getStatus()).thenReturn("added"); + when(mockFile.getPreviousFilename()).thenReturn("test-prev-meta.json"); + when(pagedFile.toList()).thenReturn(List.of(mockFile)); + + gitHubFiles = axonIvyMarketRepoServiceImpl.fetchMarketItemsBySHA1Range(startSHA1, endSHA1); + assertEquals(1, gitHubFiles.size()); + assertEquals(fileName, gitHubFiles.get(0).getFileName()); + } + + @Test + void testGetLastCommit() throws IOException { + var lastCommit = axonIvyMarketRepoServiceImpl.getLastCommit(0l); + assertEquals(null, lastCommit); + } +} diff --git a/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java new file mode 100644 index 000000000..d5d8ba716 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java @@ -0,0 +1,65 @@ +package com.axonivy.market.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.kohsuke.github.PagedIterable; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; + +@ExtendWith(MockitoExtension.class) +class GHAxonIvyProductRepoServiceImplTest { + + private static final String DUMMY_TAG = "v1.0.0"; + + @Mock + PagedIterable listTags; + + @Mock + GHRepository ghRepository; + + @Mock + GitHubService gitHubService; + + @InjectMocks + private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; + + @BeforeEach + void setup() throws IOException { + var mockGHOrganization = mock(GHOrganization.class); + when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); + when(gitHubService.getOrganization(any())).thenReturn(mockGHOrganization); + } + + @Test + void testAllTagsFromRepoName() throws IOException { + var mockTag = mock(GHTag.class); + when(mockTag.getName()).thenReturn(DUMMY_TAG); + when(listTags.toList()).thenReturn(List.of(mockTag)); + when(ghRepository.listTags()).thenReturn(listTags); + var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); + assertEquals(1, result.size()); + assertEquals(DUMMY_TAG, result.get(0).getName()); + } + + @Test + void testContentFromGHRepoAndTag() { + var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertEquals(null, result); + } +} diff --git a/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java new file mode 100644 index 000000000..159c43683 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -0,0 +1,56 @@ +package com.axonivy.market.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.github.service.impl.GitHubServiceImpl; + +@ExtendWith(MockitoExtension.class) +class GitHubServiceImplTest { + private static final String DUMMY_API_URL = "https://api.github.com"; + + @Mock + GitHub gitHub; + + @Mock + GHRepository ghRepository; + + @InjectMocks + private GitHubServiceImpl gitHubService; + + @Test + void testGetGithub() throws IOException { + var result = gitHubService.getGitHub(); + assertEquals(DUMMY_API_URL, result.getApiUrl()); + } + + @Test + void testGetGithubContent() throws IOException { + var mockGHContent = mock(GHContent.class); + final String dummryURL = DUMMY_API_URL.concat("/dummry-content"); + when(mockGHContent.getUrl()).thenReturn(dummryURL); + when(ghRepository.getFileContent(any())).thenReturn(mockGHContent); + var result = gitHubService.getGHContent(ghRepository, ""); + assertEquals(dummryURL, result.getUrl()); + } + + @Test + void testGetDirectoryContent() throws IOException { + var result = gitHubService.getDirectoryContent(ghRepository, ""); + assertEquals(0, result.size()); + } + +} diff --git a/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java new file mode 100644 index 000000000..5671a21c5 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -0,0 +1,276 @@ +package com.axonivy.market.service; + +import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.CommonConstants.META_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.FileStatus; +import com.axonivy.market.enums.FileType; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.github.model.GitHubFile; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.repository.GitHubRepoMetaRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.impl.ProductServiceImpl; + +@ExtendWith(MockitoExtension.class) +class ProductServiceImplTest { + + private static final String SAMPLE_PRODUCT_ID = "amazon-comprehend"; + private static final String SAMPLE_PRODUCT_NAME = "Amazon Comprehend"; + private static final long LAST_CHANGE_TIME = 1718096290000l; + private static final Pageable PAGEABLE = PageRequest.of(0, 20, + Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); + private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; + private String keyword; + private Page mockResultReturn; + + @Mock + private ProductRepository productRepository; + + @Mock + private GHAxonIvyMarketRepoService marketRepoService; + + @Mock + private GitHubRepoMetaRepository repoMetaRepository; + + @Mock + private GitHubService gitHubService; + + @InjectMocks + private ProductServiceImpl productService; + + @BeforeEach + public void setup() { + mockResultReturn = createPageProductsMock(); + } + + @Test + void testFindProducts() { + // Start testing by All + when(productRepository.findAll(any(Pageable.class))).thenReturn(mockResultReturn); + // Executes + var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, PAGEABLE); + assertEquals(mockResultReturn, result); + + // Start testing by Connector + when(productRepository.findByType(any(), any(Pageable.class))).thenReturn(mockResultReturn); + // Executes + result = productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, PAGEABLE); + assertEquals(mockResultReturn, result); + + // Start testing by Other + // Executes + result = productService.findProducts(TypeOption.DEMOS.getOption(), keyword, PAGEABLE); + assertEquals(0, result.getSize()); + } + + @Test + void testSyncProductsAsUpdateMetaJSONFromGitHub() throws IOException { + // Start testing by adding new meta + mockMarketRepoMetaStatus(); + var mockCommit = mockGHCommitHasSHA1(UUID.randomUUID().toString()); + when(mockCommit.getCommitDate()).thenReturn(new Date()); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + + var mockGithubFile = new GitHubFile(); + mockGithubFile.setFileName(META_FILE); + mockGithubFile.setType(FileType.META); + mockGithubFile.setStatus(FileStatus.ADDED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGithubFile)); + var mockGHContent = mockGHContentAsMetaJSON(); + when(gitHubService.getGHContent(any(), anyString())).thenReturn(mockGHContent); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + assertEquals(false, result); + + // Start testing by deleting new meta + mockCommit = mockGHCommitHasSHA1(UUID.randomUUID().toString()); + when(mockCommit.getCommitDate()).thenReturn(new Date()); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + mockGithubFile.setStatus(FileStatus.REMOVED); + // Executes + result = productService.syncLatestDataFromMarketRepo(); + assertEquals(false, result); + } + + @Test + void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { + // Start testing by adding new logo + mockMarketRepoMetaStatus(); + var mockCommit = mockGHCommitHasSHA1(UUID.randomUUID().toString()); + when(mockCommit.getCommitDate()).thenReturn(new Date()); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + + var mockGitHubFile = mock(GitHubFile.class); + mockGitHubFile = new GitHubFile(); + mockGitHubFile.setFileName(LOGO_FILE); + mockGitHubFile.setType(FileType.LOGO); + mockGitHubFile.setStatus(FileStatus.ADDED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); + var mockGHContent = mockGHContentAsMetaJSON(); + when(gitHubService.getGHContent(any(), anyString())).thenReturn(mockGHContent); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + assertEquals(false, result); + + // Start testing by deleting new logo + when(mockCommit.getSHA1()).thenReturn(UUID.randomUUID().toString()); + mockGitHubFile.setStatus(FileStatus.REMOVED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); + when(gitHubService.getGHContent(any(), anyString())).thenReturn(mockGHContent); + when(productRepository.findByLogoUrl(any())).thenReturn(new Product()); + + // Executes + result = productService.syncLatestDataFromMarketRepo(); + assertEquals(false, result); + } + + @Test + void testFindAllProductsWithKeyword() throws IOException { + when(productRepository.findAll(any(Pageable.class))).thenReturn(mockResultReturn); + // Executes + var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, PAGEABLE); + assertEquals(mockResultReturn, result); + verify(productRepository).findAll(any(Pageable.class)); + + // Test has keyword + when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(Pageable.class))) + .thenReturn(new PageImpl<>(mockResultReturn.stream() + .filter(product -> product.getName().equals(SAMPLE_PRODUCT_NAME)).collect(Collectors.toList()))); + // Executes + result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, PAGEABLE); + verify(productRepository).findAll(any(Pageable.class)); + assertTrue(result.hasContent()); + assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getName()); + + // Test has keyword and type is connector + when(productRepository.searchByKeywordAndType(any(), any(), any(Pageable.class))).thenReturn( + new PageImpl<>(mockResultReturn.stream().filter(product -> product.getName().equals(SAMPLE_PRODUCT_NAME) + && product.getType().equals(TypeOption.CONNECTORS.getCode())).collect(Collectors.toList()))); + // Executes + result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, PAGEABLE); + assertTrue(result.hasContent()); + assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getName()); + } + + @Test + void testSyncProductsFirstTime() throws IOException { + var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + + var mockContent = mockGHContentAsMetaJSON(); + InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); + when(mockContent.read()).thenReturn(inputStream); + + Map> mockGHContentMap = new HashMap<>(); + mockGHContentMap.put(SAMPLE_PRODUCT_ID, List.of(mockContent)); + when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + assertEquals(false, result); + } + + @Test + void testNothingToSync() throws IOException { + var gitHubRepoMeta = mock(GitHubRepoMeta.class); + when(gitHubRepoMeta.getLastSHA1()).thenReturn(SHA1_SAMPLE); + var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + when(repoMetaRepository.findByRepoName(anyString())).thenReturn(gitHubRepoMeta); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + assertEquals(true, result); + } + + @Test + void testSearchProducts() { + var simplePageable = PageRequest.of(0, 20); + String type = TypeOption.ALL.getOption(); + keyword = "on"; + when(productRepository.searchByNameOrShortDescriptionRegex(keyword, simplePageable)).thenReturn(mockResultReturn); + + var result = productService.findProducts(type, keyword, simplePageable); + assertEquals(result, mockResultReturn); + verify(productRepository).searchByNameOrShortDescriptionRegex(keyword, simplePageable); + } + + private Page createPageProductsMock() { + var mockProducts = new ArrayList(); + Product mockProduct = new Product(); + mockProduct.setId(SAMPLE_PRODUCT_ID); + mockProduct.setName(SAMPLE_PRODUCT_NAME); + mockProduct.setType("connector"); + mockProducts.add(mockProduct); + + mockProduct = new Product(); + mockProduct.setId("tel-search-ch-connector"); + mockProduct.setName("Swiss phone directory"); + mockProduct.setType("util"); + mockProducts.add(mockProduct); + return new PageImpl<>(mockProducts); + } + + private void mockMarketRepoMetaStatus() { + var mockMartketRepoMeta = new GitHubRepoMeta(); + mockMartketRepoMeta.setRepoURL(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); + mockMartketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); + mockMartketRepoMeta.setLastChange(LAST_CHANGE_TIME); + mockMartketRepoMeta.setLastSHA1(SHA1_SAMPLE); + when(repoMetaRepository.findByRepoName(any())).thenReturn(mockMartketRepoMeta); + } + + private GHCommit mockGHCommitHasSHA1(String sha1) { + var mockCommit = mock(GHCommit.class); + when(mockCommit.getSHA1()).thenReturn(sha1); + return mockCommit; + } + + private GHContent mockGHContentAsMetaJSON() { + var mockGHContent = mock(GHContent.class); + when(mockGHContent.getName()).thenReturn(META_FILE); + return mockGHContent; + } +} diff --git a/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java new file mode 100644 index 000000000..f23d6ea21 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -0,0 +1,26 @@ +package com.axonivy.market.service; + +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; + +import org.awaitility.Awaitility; +import org.awaitility.Durations; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import com.axonivy.market.schedulingtask.ScheduledTasks; + +@SpringBootTest +class SchedulingTasksTest { + + @SpyBean + ScheduledTasks tasks; + + @Test + void testShouldNotTriggerAfterApplicationStarted() { + Awaitility.await().atMost(Durations.TEN_SECONDS).untilAsserted(() -> { + verify(tasks, atLeast(0)).syncDataForProductFromGitHubRepo(); + }); + } +} diff --git a/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/src/test/java/com/axonivy/market/service/UserServiceImplTest.java index 102c6f3c7..d14aaff8c 100644 --- a/src/test/java/com/axonivy/market/service/UserServiceImplTest.java +++ b/src/test/java/com/axonivy/market/service/UserServiceImplTest.java @@ -15,7 +15,7 @@ import com.axonivy.market.service.impl.UserServiceImpl; @ExtendWith(MockitoExtension.class) -public class UserServiceImplTest { +class UserServiceImplTest { @InjectMocks private UserServiceImpl employeeService; @@ -24,7 +24,7 @@ public class UserServiceImplTest { private UserRepository userRepository; @Test - public void testFindAllUser() { + void testFindAllUser() { // Mock data and service User mockUser = new User(); mockUser.setId("123"); diff --git a/src/test/resources/meta.json b/src/test/resources/meta.json new file mode 100644 index 000000000..90fb8f309 --- /dev/null +++ b/src/test/resources/meta.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.axonivy.com/market/10.0.0/meta.json", + "id": "amazon-comprehend", + "name": "Amazon Comprehend", + "description": "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", + "type": "connector", + "platformReview": "4.5", + "sourceUrl": "https://github.com/axonivy-market/amazon-comprehend-connector", + "statusBadgeUrl": "https://github.com/axonivy-market/amazon-comprehend-connector/actions/workflows/ci.yml/badge.svg", + "language": "English", + "industry": "Cross-Industry", + "tags": [ + "AI" + ], + "mavenArtifacts": [ + { + "repoUrl": "https://maven.axonivy.com", + "name": "Amazon Comprehend Product", + "groupId": "com.axonivy.connector.amazon.comprehend", + "artifactId": "amazon-comprehend-connector-product", + "type": "zip" + } + ] +} \ No newline at end of file From fe0ab13f7443da57a9ee61f01be001b38babc044 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:25:15 +0700 Subject: [PATCH 07/62] MARP-394 MP api to fetch all artifacts and search (#21) * Add name for param * Rename app name * Add param name * Remove required for pageable * New Gh token --- .github/workflows/dev-build.yml | 12 ++++++++---- .../axonivy/market/controller/ProductController.java | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index db32f52fe..137e06a6c 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -19,19 +19,23 @@ jobs: java-version: '17' distribution: 'temurin' cache: maven - - name: Update MongoDB in application.properties + - name: Update configuration env: + APP_PROPERTIES_FILE: 'src/main/resources/application.properties' + GITHUB_TOKEN_FILE: 'src/main/resources/github.token' MONGODB_HOST: ${{ secrets.MONGODB_HOST }} MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | - sed -i "s/^spring.data.mongodb.host=.*$/spring.data.mongodb.host=$MONGODB_HOST/" src/main/resources/application.properties - sed -i "s/^spring.data.mongodb.database=.*$/spring.data.mongodb.database=$MONGODB_DATABASE/" src/main/resources/application.properties + sed -i "s/^spring.data.mongodb.host=.*$/spring.data.mongodb.host=$MONGODB_HOST/" $APP_PROPERTIES_FILE + sed -i "s/^spring.data.mongodb.database=.*$/spring.data.mongodb.database=$MONGODB_DATABASE/" $APP_PROPERTIES_FILE + sed -i '1d;$d' $GITHUB_TOKEN_FILE && echo $GH_TOKEN > $GITHUB_TOKEN_FILE - name: Build with Maven run: mvn clean package -DskipTests - name: Prepare deployment directory run: mkdir -p deployment && cp target/*.war deployment/ - name: Copy WAR to Tomcat server - run: sudo cp deployment/*.war /opt/tomcat/webapps/marketplace-server.war + run: sudo cp deployment/*.war /opt/tomcat/webapps/marketplace-service.war - name: Restart Tomcat server run: | sudo systemctl stop tomcat diff --git a/src/main/java/com/axonivy/market/controller/ProductController.java b/src/main/java/com/axonivy/market/controller/ProductController.java index 3f126c9f7..434beb202 100644 --- a/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/src/main/java/com/axonivy/market/controller/ProductController.java @@ -43,8 +43,9 @@ public ProductController(ProductService service, ProductModelAssembler assembler @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() - public ResponseEntity> findProducts(@RequestParam(required = true) String type, - @RequestParam(required = false) String keyword, Pageable pageable) { + public ResponseEntity> findProducts( + @RequestParam(required = true, name = "type") String type, + @RequestParam(required = false, name = "keyword") String keyword, Pageable pageable) { Page results = service.findProducts(type, keyword, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); From 47b780e8157d0bda822fd36b56257e4077f71e8f Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Tue, 25 Jun 2024 11:57:05 +0700 Subject: [PATCH 08/62] MARP-394 MP APIs to fetch all artifacts and search - Fix new line in token --- .../axonivy/market/github/service/impl/GitHubServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index a5bea0728..e4f38e7e5 100644 --- a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -25,7 +25,7 @@ public class GitHubServiceImpl implements GitHubService { public GitHub getGitHub() throws IOException { File gitHubToken = ResourceUtils.getFile(GITHUB_TOKEN_FILE); var token = Files.readString(gitHubToken.toPath()); - return new GitHubBuilder().withOAuthToken(token).build(); + return new GitHubBuilder().withOAuthToken(token.trim().strip()).build(); } @Override From e3fae620056b8a8addf39d00d916ac66f41b331c Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Fri, 28 Jun 2024 10:09:28 +0700 Subject: [PATCH 09/62] Update README.md for githib.token --- README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 68876d06f..4d546f33a 100644 --- a/README.md +++ b/README.md @@ -11,26 +11,24 @@ For further reference, please consider the following sections: ### Guides The following guides illustrate how to use some features concretely: -* installing mongodb , and access it mongodb://localhost:27017/ in mongodb compass or studio3T -* run "mvn clean install" to build a project -* run "mvn test" to test all Test class - -### MongoDB's property configs -* We can set up properties in class application.properties and MongoConfig - -### Access Swagger URL: http://{your-host}/swagger-ui/index.html - -### Steps to set up: * Installing mongodb, and access it as Url mongodb://localhost:27017/, and you can create and name whatever you want ,then you should put them to application.properties +* You can change the MongoDB configuration in file `application.properties` + ``` + spring.data.mongodb.host= + spring.data.mongodb.database= + ``` +* Update GitHub token in file `github.token` * Run mvn clean install to build project * Run mvn test to test all tests -* You can change the configuration in file “application.properties“ -### In case of using eclipse you should install manually Lombok . + +### Access Swagger URL: http://{your-host}/swagger-ui/index.html + +### Install Lombok for Eclipse IDE * Download lombok here https://projectlombok.org/download * run command "java -jar lombok.jar" then you can access file “eclipse.ini“ in eclipse folder where you install → there is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are successful * Start eclipse * Import the project then in the eclipse , you should run the command “mvn clean install“ * After that you go to class MarketplaceServiceApplication → right click to main method → click run as → choose Java Application -* Then you can send a request in postman :) +* Then you can send a request in postman * If you want to run single test in class UserServiceImplTest. You can right-click to method testFindAllUser and right click → select Run as → choose JUnit Test \ No newline at end of file From 0967f381451b286e4fa066bbe723b0ac244167c8 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:02:52 +0800 Subject: [PATCH 10/62] feature/Marp-475 Detail pages for new market installation and download section --- .../ArchivedArtifactsComparator.java | 14 + .../comparator/LatestVersionComparator.java | 32 + .../config/MarketApiDocumentConfig.java | 2 +- .../config/MarketHeaderInterceptor.java | 2 +- .../axonivy/market/config/MongoConfig.java | 61 +- .../com/axonivy/market/config/WebConfig.java | 2 +- .../market/constants/EntityConstants.java | 7 +- .../market/constants/GitHubConstants.java | 9 +- .../market/constants/MavenConstants.java | 19 + .../NonStandardProductPackageConstants.java | 20 + .../constants/ProductJsonConstants.java | 21 + .../constants/RequestMappingConstants.java | 2 +- .../controller/ProductDetailsController.java | 36 +- .../market/controller/UserController.java | 13 +- .../market/entity/MavenArtifactModel.java | 39 ++ .../market/entity/MavenArtifactVersion.java | 32 + .../com/axonivy/market/entity/Product.java | 78 +-- .../market/factory/ProductFactory.java | 123 ++-- .../market/github/model/ArchivedArtifact.java | 24 + .../market/github/model/MavenArtifact.java | 22 +- .../service/GHAxonIvyProductRepoService.java | 5 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 148 +++-- .../service/impl/GitHubServiceImpl.java | 2 +- .../model/MavenArtifactVersionModel.java | 18 + .../axonivy/market/model/ProductModel.java | 34 +- .../MavenArtifactVersionRepository.java | 9 + .../market/repository/ProductRepository.java | 4 + .../market/service/VersionService.java | 17 + .../service/impl/VersionServiceImpl.java | 350 +++++++++++ .../axonivy/market/utils/XmlReaderUtils.java | 59 ++ .../market/controller/AppControllerTest.java | 3 +- .../controller/ProductControllerTest.java | 2 +- .../ProductDetailsControllerTest.java | 23 +- .../market/factory/ProductFactoryTest.java | 79 ++- .../GHAxonIvyProductRepoServiceImplTest.java | 242 ++++++++ .../GHAxonIvyProductRepoServiceImplTest.java | 65 -- .../market/service/GitHubServiceImplTest.java | 3 +- .../service/VersionServiceImplTest.java | 565 ++++++++++++++++++ .../market/utils/XmlReaderUtilsTest.java | 21 + 39 files changed, 1903 insertions(+), 304 deletions(-) create mode 100644 src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java create mode 100644 src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java create mode 100644 src/main/java/com/axonivy/market/constants/MavenConstants.java create mode 100644 src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java create mode 100644 src/main/java/com/axonivy/market/constants/ProductJsonConstants.java create mode 100644 src/main/java/com/axonivy/market/entity/MavenArtifactModel.java create mode 100644 src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java create mode 100644 src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java create mode 100644 src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java create mode 100644 src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java create mode 100644 src/main/java/com/axonivy/market/service/VersionService.java create mode 100644 src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java create mode 100644 src/main/java/com/axonivy/market/utils/XmlReaderUtils.java create mode 100644 src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java delete mode 100644 src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java create mode 100644 src/test/java/com/axonivy/market/service/VersionServiceImplTest.java create mode 100644 src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java diff --git a/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java b/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java new file mode 100644 index 000000000..7a27d7718 --- /dev/null +++ b/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java @@ -0,0 +1,14 @@ +package com.axonivy.market.comparator; + +import com.axonivy.market.github.model.ArchivedArtifact; + +import java.util.Comparator; + +public class ArchivedArtifactsComparator implements Comparator { + private final LatestVersionComparator comparator = new LatestVersionComparator(); + + @Override + public int compare(ArchivedArtifact artifact1, ArchivedArtifact artifact2) { + return comparator.compare(artifact1.getLastVersion(), artifact2.getLastVersion()); + } +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java b/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java new file mode 100644 index 000000000..9419b411f --- /dev/null +++ b/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java @@ -0,0 +1,32 @@ +package com.axonivy.market.comparator; + +import java.util.Comparator; + +public class LatestVersionComparator implements Comparator { + + @Override + public int compare(String v1, String v2) { + // Split by "." + String[] parts1 = v1.split("\\."); + String[] parts2 = v2.split("\\."); + + // Compare up to the shorter length + int length = Math.min(parts1.length, parts2.length); + for (int i = 0; i < length; i++) { + try { + int num1 = Integer.parseInt(parts1[i]); + int num2 = Integer.parseInt(parts2[i]); + // Return difference for numeric parts + if (num1 != num2) { + return num2 - num1; + } + // Handle non-numeric parts (e.g., "m229") + } catch (NumberFormatException e) { + return parts2[i].replaceAll("\\D", "").compareTo(parts1[i].replaceAll("\\D", "")); + } + } + + // Versions with more parts are considered larger + return parts2.length - parts1.length; + } +} diff --git a/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java index a92282a64..61b6a0773 100644 --- a/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java +++ b/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -37,4 +37,4 @@ private OpenApiCustomizer customMarketHeaders() { } }); } -} +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java index 1d35cb9cc..963706069 100644 --- a/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java +++ b/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java @@ -27,4 +27,4 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } return true; } -} +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/config/MongoConfig.java b/src/main/java/com/axonivy/market/config/MongoConfig.java index 36b7b6f1d..a6cd2bc05 100644 --- a/src/main/java/com/axonivy/market/config/MongoConfig.java +++ b/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -1,8 +1,15 @@ package com.axonivy.market.config; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import com.mongodb.ConnectionString; @@ -14,23 +21,39 @@ @EnableMongoRepositories(basePackages = "com.axonivy.market.repository") public class MongoConfig extends AbstractMongoClientConfiguration { - @Value("${spring.data.mongodb.host}") - private String host; - - @Value("${spring.data.mongodb.database}") - private String databaseName; - - @Override - protected String getDatabaseName() { - return databaseName; - } - - @Override - public MongoClient mongoClient() { - ConnectionString connectionString = new ConnectionString(host); - MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString) - .build(); - - return MongoClients.create(mongoClientSettings); - } + @Value("${spring.data.mongodb.host}") + private String host; + + @Value("${spring.data.mongodb.database}") + private String databaseName; + + @Override + protected String getDatabaseName() { + return databaseName; + } + + @Override + public MongoClient mongoClient() { + ConnectionString connectionString = new ConnectionString(host); + MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString) + .build(); + + return MongoClients.create(mongoClientSettings); + } + + /** + * By default, the key in hash map is not allow to contain dot character (.) we + * need to escape it by define a replacement to that char + **/ + @Override + @Bean + public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, + MongoCustomConversions customConversions, MongoMappingContext mappingContext) { + DbRefResolver dbRefResolver = new DefaultDbRefResolver(databaseFactory); + MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext); + converter.setCustomConversions(customConversions); + converter.setCodecRegistryProvider(databaseFactory); + converter.setMapKeyDotReplacement("_"); + return converter; + } } diff --git a/src/main/java/com/axonivy/market/config/WebConfig.java b/src/main/java/com/axonivy/market/config/WebConfig.java index b885a477d..b9d75afa3 100644 --- a/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/src/main/java/com/axonivy/market/config/WebConfig.java @@ -39,4 +39,4 @@ public void addCorsMappings(CorsRegistry registry) { .allowedHeaders(ALLOWED_HEADERS) .maxAge(marketCorsAllowedOriginMaxAge); } -} +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/constants/EntityConstants.java b/src/main/java/com/axonivy/market/constants/EntityConstants.java index eaf213b1d..76c1c45ab 100644 --- a/src/main/java/com/axonivy/market/constants/EntityConstants.java +++ b/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -5,7 +5,8 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class EntityConstants { - public static final String USER = "User"; - public static final String PRODUCT = "Product"; - public static final String GH_REPO_META = "GitHubRepoMeta"; + public static final String USER = "User"; + public static final String PRODUCT = "Product"; + public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; + public static final String GH_REPO_META = "GitHubRepoMeta"; } diff --git a/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/src/main/java/com/axonivy/market/constants/GitHubConstants.java index 22ce4a3bf..c03c69f65 100644 --- a/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -5,7 +5,8 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubConstants { - public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; - public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; - public static final String AXONIVY_MARKETPLACE_PATH = "market"; -} + public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; + public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; + public static final String AXONIVY_MARKETPLACE_PATH = "market"; + public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/constants/MavenConstants.java b/src/main/java/com/axonivy/market/constants/MavenConstants.java new file mode 100644 index 000000000..992a4289b --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/MavenConstants.java @@ -0,0 +1,19 @@ +package com.axonivy.market.constants; + +public class MavenConstants { + private MavenConstants() { + } + + public static final String SNAPSHOT_RELEASE_POSTFIX = "-SNAPSHOT"; + public static final String SPRINT_RELEASE_POSTFIX = "-m"; + public static final String PRODUCT_ARTIFACT_POSTFIX = "-product"; + public static final String METADATA_URL_FORMAT = "%s/%s/%s/maven-metadata.xml"; + public static final String DEFAULT_IVY_MAVEN_BASE_URL = "https://maven.axonivy.com"; + public static final String DOT_SEPARATOR = "."; + public static final String GROUP_ID_URL_SEPARATOR = "/"; + public static final String ARTIFACT_ID_SEPARATOR = "-"; + public static final String ARTIFACT_NAME_SEPARATOR = " "; + public static final String ARTIFACT_DOWNLOAD_URL_FORMAT = "%s/%s/%s/%s/%s-%s.%s"; + public static final String ARTIFACT_NAME_FORMAT = "%s (%s)"; + public static final String VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE = "//versions/version/text()"; +} diff --git a/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java b/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java new file mode 100644 index 000000000..c2e14744e --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java @@ -0,0 +1,20 @@ +package com.axonivy.market.constants; + +public class NonStandardProductPackageConstants { + private NonStandardProductPackageConstants() { + } + + public static final String PORTAL = "portal"; + public static final String MICROSOFT_365 = ""; // No meta.json + public static final String MICROSOFT_CALENDAR = "msgraph-calendar"; // no fix product json + public static final String MICROSOFT_MAIL = "msgraph-mail";// no fix product json + public static final String MICROSOFT_TEAMS = "msgraph-chat";// no fix product json + public static final String MICROSOFT_TODO = "msgraph-todo";// no fix product json + public static final String CONNECTIVITY_FEATURE = "connectivity-demo"; + public static final String EMPLOYEE_ONBOARDING = "employee-onboarding"; // Invalid meta.json + public static final String ERROR_HANDLING = "error-handling-demo"; + public static final String RULE_ENGINE_DEMOS = "rule-engine-demo"; + public static final String WORKFLOW_DEMO = "workflow-demo"; + public static final String HTML_DIALOG_DEMO = "html-dialog-demo"; + public static final String PROCESSING_VALVE_DEMO = "processing-valve-demo";// no product json +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java new file mode 100644 index 000000000..9ce62f956 --- /dev/null +++ b/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java @@ -0,0 +1,21 @@ +package com.axonivy.market.constants; + +public class ProductJsonConstants { + + public static final String DATA = "data"; + public static final String REPOSITORIES = "repositories"; + public static final String URL = "url"; + public static final String ID = "id"; + public static final String PROJECTS = "projects"; + public static final String ARTIFACT_ID = "artifactId"; + public static final String GROUP_ID = "groupId"; + public static final String TYPE = "type"; + public static final String DEPENDENCIES = "dependencies"; + public static final String INSTALLERS = "installers"; + public static final String MAVEN_IMPORT_INSTALLER_ID = "maven-import"; + public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; + public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; + + private ProductJsonConstants() { + } +} diff --git a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index b3c02f12c..f4687a442 100644 --- a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -12,4 +12,4 @@ public class RequestMappingConstants { public static final String PRODUCT = API + "/product"; public static final String PRODUCT_DETAILS = API + "/product-details"; public static final String SWAGGER_URL = "/swagger-ui/index.html"; -} +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index a9e8f3647..c5536cd48 100644 --- a/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -1,22 +1,40 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; - +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.service.VersionService; import org.springframework.http.HttpStatus; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; @RestController @RequestMapping(PRODUCT_DETAILS) public class ProductDetailsController { + private final VersionService service; + + public ProductDetailsController(VersionService service) { + this.service = service; + } + + @GetMapping("/{id}") + public ResponseEntity findProduct(@PathVariable("id") String key, + @RequestParam(name = "type", required = false) String type) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } - @GetMapping("/{id}") - public ResponseEntity findProduct(@PathVariable("id") String key, - @RequestParam(name = "type", required = false) String type) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } -} + @GetMapping("/{id}/versions") + public ResponseEntity> findProductVersionsById(@PathVariable("id") String id, + @RequestParam(name = "isShowDevVersion") boolean isShowDevVersion, + @RequestParam(name = "designerVersion", required = false) String designerVersion) { + List models = service.getArtifactsAndVersionToDisplay(id, isShowDevVersion, + designerVersion); + return new ResponseEntity<>(models, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/controller/UserController.java b/src/main/java/com/axonivy/market/controller/UserController.java index f94d46920..c83c7cc2b 100644 --- a/src/main/java/com/axonivy/market/controller/UserController.java +++ b/src/main/java/com/axonivy/market/controller/UserController.java @@ -1,16 +1,15 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; - -import java.util.List; - +import com.axonivy.market.entity.User; +import com.axonivy.market.service.UserService; 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.axonivy.market.entity.User; -import com.axonivy.market.service.UserService; +import java.util.List; + +import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; @RestController @RequestMapping(USER_MAPPING) @@ -25,4 +24,4 @@ public UserController(UserService userService) { public ResponseEntity> getAllUser() { return ResponseEntity.ok(userService.getAllUsers()); } -} +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java new file mode 100644 index 000000000..f3f8977d5 --- /dev/null +++ b/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java @@ -0,0 +1,39 @@ +package com.axonivy.market.entity; + +import com.axonivy.market.github.model.MavenArtifact; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Transient; + +import java.io.Serializable; +import java.util.Objects; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@Getter +public class MavenArtifactModel implements Serializable { + private static final long serialVersionUID = 1L; + private String name; + private String downloadUrl; + @Transient + private Boolean isProductArtifact; + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) { + return false; + } + MavenArtifactModel reference = (MavenArtifactModel) object; + return Objects.equals(name, reference.getName()) && Objects.equals(downloadUrl, reference.getDownloadUrl()); + } + + @Override + public int hashCode() { + return Objects.hash(name, downloadUrl); + } +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java b/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java new file mode 100644 index 000000000..f92be46fc --- /dev/null +++ b/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java @@ -0,0 +1,32 @@ +package com.axonivy.market.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.axonivy.market.constants.EntityConstants.MAVEN_ARTIFACT_VERSION; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Document(MAVEN_ARTIFACT_VERSION) +public class MavenArtifactVersion implements Serializable { + @Id + private String productId; + private List versions = new ArrayList<>(); + private Map> productArtifactWithVersionReleased = new HashMap<>(); + + public MavenArtifactVersion(String productId) { + this.productId = productId; + } +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/entity/Product.java b/src/main/java/com/axonivy/market/entity/Product.java index 75cba5e35..f38748c41 100644 --- a/src/main/java/com/axonivy/market/entity/Product.java +++ b/src/main/java/com/axonivy/market/entity/Product.java @@ -6,6 +6,7 @@ import java.util.Date; import java.util.List; +import com.axonivy.market.github.model.MavenArtifact; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.data.annotation.Id; @@ -23,45 +24,46 @@ @Document(PRODUCT) public class Product implements Serializable { - private static final long serialVersionUID = -8770801877877277258L; - @Id - private String id; - private String marketDirectory; - private String name; - private String version; - private String shortDescription; - private String logoUrl; - private Boolean listed; - private String type; - private List tags; - private String vendor; - private String vendorImage; - private String vendorUrl; - private String platformReview; - private String cost; - private String repositoryName; - private String sourceUrl; - private String statusBadgeUrl; - private String language; - private String industry; - private String compatibility; - private Boolean validate; - private Boolean contactUs; - private Integer installationCount; - private Date newestPublishedDate; - private String newestReleaseVersion; + private static final long serialVersionUID = -8770801877877277258L; + @Id + private String id; + private String marketDirectory; + private String name; + private String version; + private String shortDescription; + private String logoUrl; + private Boolean listed; + private String type; + private List tags; + private String vendor; + private String vendorImage; + private String vendorUrl; + private String platformReview; + private String cost; + private String repositoryName; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private String compatibility; + private Boolean validate; + private Boolean contactUs; + private Integer installationCount; + private Date newestPublishedDate; + private String newestReleaseVersion; + private List artifacts; - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); - } + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); + } } \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/factory/ProductFactory.java b/src/main/java/com/axonivy/market/factory/ProductFactory.java index da55fbf09..8ec75e239 100644 --- a/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -22,72 +22,73 @@ @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ProductFactory { - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = new ObjectMapper(); - public static Product mappingByGHContent(Product product, GHContent content) { - if (content == null) { - return product; - } + public static Product mappingByGHContent(Product product, GHContent content) { + if (content == null) { + return product; + } - var contentName = content.getName(); - if (StringUtils.endsWith(contentName, META_FILE)) { - mappingByMetaJSONFile(product, content); - } - if (StringUtils.endsWith(contentName, LOGO_FILE)) { - product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); - } - return product; - } + var contentName = content.getName(); + if (StringUtils.endsWith(contentName, META_FILE)) { + mappingByMetaJSONFile(product, content); + } + if (StringUtils.endsWith(contentName, LOGO_FILE)) { + product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); + } + return product; + } - public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { - Meta meta = null; - try { - meta = jsonDecode(ghContent); - } catch (Exception e) { - log.error("Mapping from Meta file by GHContent failed", e); - return product; - } + public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { + Meta meta = null; + try { + meta = jsonDecode(ghContent); + } catch (Exception e) { + log.error("Mapping from Meta file by GHContent failed", e); + return product; + } - product.setId(meta.getId()); - product.setName(meta.getName()); - product.setMarketDirectory(extractParentDirectory(ghContent)); - product.setListed(meta.getListed()); - product.setType(meta.getType()); - product.setTags(meta.getTags()); - product.setVersion(meta.getVersion()); - product.setShortDescription(meta.getDescription()); - product.setVendor(meta.getVendor()); - product.setVendorImage(meta.getVendorImage()); - product.setVendorUrl(meta.getVendorUrl()); - product.setPlatformReview(meta.getPlatformReview()); - product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); - product.setLanguage(meta.getLanguage()); - product.setIndustry(meta.getIndustry()); - extractSourceUrl(product, meta); - return product; - } + product.setId(meta.getId()); + product.setName(meta.getName()); + product.setMarketDirectory(extractParentDirectory(ghContent)); + product.setListed(meta.getListed()); + product.setType(meta.getType()); + product.setTags(meta.getTags()); + product.setVersion(meta.getVersion()); + product.setShortDescription(meta.getDescription()); + product.setVendor(meta.getVendor()); + product.setVendorImage(meta.getVendorImage()); + product.setVendorUrl(meta.getVendorUrl()); + product.setPlatformReview(meta.getPlatformReview()); + product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); + product.setLanguage(meta.getLanguage()); + product.setIndustry(meta.getIndustry()); + extractSourceUrl(product, meta); + product.setArtifacts(meta.getMavenArtifacts()); + return product; + } - private static String extractParentDirectory(GHContent ghContent) { - var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); - return path.replace(ghContent.getName(), EMPTY); - } + private static String extractParentDirectory(GHContent ghContent) { + var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); + return path.replace(ghContent.getName(), EMPTY); + } - private static void extractSourceUrl(Product product, Meta meta) { - var sourceUrl = meta.getSourceUrl(); - if (StringUtils.isBlank(sourceUrl)) { - return; - } - String[] tokens = sourceUrl.split(SLASH); - var tokensLength = tokens.length; - var repositoryPath = sourceUrl; - if (tokensLength > 1) { - repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); - } - product.setRepositoryName(repositoryPath); - product.setSourceUrl(sourceUrl); - } + public static void extractSourceUrl(Product product, Meta meta) { + var sourceUrl = meta.getSourceUrl(); + if (StringUtils.isBlank(sourceUrl)) { + return; + } + String[] tokens = sourceUrl.split(SLASH); + var tokensLength = tokens.length; + var repositoryPath = sourceUrl; + if (tokensLength > 1) { + repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); + } + product.setRepositoryName(repositoryPath); + product.setSourceUrl(sourceUrl); + } - private static Meta jsonDecode(GHContent ghContent) throws IOException { - return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); - } + private static Meta jsonDecode(GHContent ghContent) throws IOException { + return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); + } } diff --git a/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java new file mode 100644 index 000000000..1bde19a0e --- /dev/null +++ b/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java @@ -0,0 +1,24 @@ +package com.axonivy.market.github.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ArchivedArtifact implements Serializable { + private static final long serialVersionUID = 1L; + private String lastVersion; + private String groupId; + private String artifactId; +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/github/model/MavenArtifact.java b/src/main/java/com/axonivy/market/github/model/MavenArtifact.java index 14af356b8..811b7917b 100644 --- a/src/main/java/com/axonivy/market/github/model/MavenArtifact.java +++ b/src/main/java/com/axonivy/market/github/model/MavenArtifact.java @@ -2,11 +2,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.annotation.Transient; + +import java.io.Serializable; +import java.util.List; @Getter @Setter @@ -14,10 +17,15 @@ @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) -public class MavenArtifact { - private String repoUrl; - private String name; - private String groupId; - private String artifactId; - private String type; +public class MavenArtifact implements Serializable { + private static final long serialVersionUID = 1L; + private String repoUrl; + private String name; + private String groupId; + private String artifactId; + private String type; + private Boolean isDependency; + @Transient + private Boolean isProductArtifact; + private List archivedArtifacts; } diff --git a/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index 1665799de..3a9e85180 100644 --- a/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -1,5 +1,6 @@ package com.axonivy.market.github.service; +import com.axonivy.market.github.model.MavenArtifact; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHTag; @@ -11,4 +12,6 @@ public interface GHAxonIvyProductRepoService { GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion); List getAllTagsFromRepoName(String repoName) throws IOException; -} + + List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index def4bd9b4..6b6b8c38e 100644 --- a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -1,49 +1,127 @@ package com.axonivy.market.github.service.impl; -import java.io.IOException; -import java.util.List; - +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.log4j.Log4j2; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHTag; import org.springframework.stereotype.Service; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; -import lombok.extern.log4j.Log4j2; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; @Log4j2 @Service public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { - private GHOrganization organization; - - private final GitHubService gitHubService; - - public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { - this.gitHubService = gitHubService; - } - - @Override - public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { - try { - return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); - } catch (IOException e) { - log.error("Cannot Get Content From File Directory", e); - return null; - } - } - - private GHOrganization getOrganization() throws IOException { - if (organization == null) { - organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); - } - return organization; - } - - @Override - public List getAllTagsFromRepoName(String repoName) throws IOException { - return getOrganization().getRepository(repoName).listTags().toList(); - } + private GHOrganization organization; + private final GitHubService gitHubService; + private String repoUrl; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { + this.gitHubService = gitHubService; + } + + @Override + public List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { + List artifacts = new ArrayList<>(); + InputStream contentStream = extractedContentStream(content); + if (Objects.isNull(contentStream)) { + return artifacts; + } + + JsonNode rootNode = objectMapper.readTree(contentStream); + JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); + + for (JsonNode mavenNode : installersNode) { + JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); + + // Not convert to artifact if id of node is not maven-import or maven-dependency + List installerIdsToDisplay = List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, + ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); + if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { + continue; + } + + // Extract repository URL + JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); + repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); + + // Process projects + if (dataNode.has(ProductJsonConstants.PROJECTS)) { + extractMavenArtifactFromJsonNode(dataNode, false, artifacts); + } + + // Process dependencies + if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { + extractMavenArtifactFromJsonNode(dataNode, true, artifacts); + } + } + return artifacts; + } + + public InputStream extractedContentStream(GHContent content) { + try { + return content.read(); + } catch (IOException | NullPointerException e) { + log.warn("Can not read the current content: {}", e.getMessage()); + return null; + } + } + + public void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, + List artifacts) { + String nodeName = ProductJsonConstants.PROJECTS; + if (isDependency) { + nodeName = ProductJsonConstants.DEPENDENCIES; + } + JsonNode dependenciesNode = dataNode.path(nodeName); + for (JsonNode dependencyNode : dependenciesNode) { + MavenArtifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); + artifacts.add(artifact); + } + } + + public MavenArtifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { + MavenArtifact artifact = new MavenArtifact(); + artifact.setRepoUrl(repoUrl); + artifact.setIsDependency(isDependency); + artifact.setGroupId(node.path(ProductJsonConstants.GROUP_ID).asText()); + artifact.setArtifactId(node.path(ProductJsonConstants.ARTIFACT_ID).asText()); + artifact.setType(node.path(ProductJsonConstants.TYPE).asText()); + artifact.setIsProductArtifact(true); + return artifact; + } + + @Override + public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { + try { + return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); + } catch (IOException e) { + log.error("Cannot Get Content From File Directory", e); + return null; + } + } + + public GHOrganization getOrganization() throws IOException { + if (organization == null) { + organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + } + return organization; + } + + @Override + public List getAllTagsFromRepoName(String repoName) throws IOException { + return getOrganization().getRepository(repoName).listTags().toList(); + } } diff --git a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index e4f38e7e5..b1a069ed5 100644 --- a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -49,4 +49,4 @@ public GHContent getGHContent(GHRepository ghRepository, String path) throws IOE Assert.notNull(ghRepository, "Repository must not be null"); return ghRepository.getFileContent(path); } -} +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java b/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java new file mode 100644 index 000000000..3cb4ee1d7 --- /dev/null +++ b/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java @@ -0,0 +1,18 @@ +package com.axonivy.market.model; + +import com.axonivy.market.entity.MavenArtifactModel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class MavenArtifactVersionModel { + private String version; + private List artifactsByVersion; +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/model/ProductModel.java b/src/main/java/com/axonivy/market/model/ProductModel.java index e088f1230..f11a62244 100644 --- a/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/src/main/java/com/axonivy/market/model/ProductModel.java @@ -20,23 +20,23 @@ @Relation(collectionRelation = "products", itemRelation = "product") @JsonInclude(Include.NON_NULL) public class ProductModel extends RepresentationModel { - private String id; - private String name; - private String shortDescription; - private String logoUrl; - private String type; - private List tags; + private String id; + private String name; + private String shortDescription; + private String logoUrl; + private String type; + private List tags; - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((ProductModel) obj).getId()).isEquals(); - } + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((ProductModel) obj).getId()).isEquals(); + } } diff --git a/src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java b/src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java new file mode 100644 index 000000000..15dd9cb03 --- /dev/null +++ b/src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java @@ -0,0 +1,9 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.MavenArtifactVersion; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MavenArtifactVersionRepository extends MongoRepository { +} diff --git a/src/main/java/com/axonivy/market/repository/ProductRepository.java b/src/main/java/com/axonivy/market/repository/ProductRepository.java index 238cd774f..1b18bad04 100644 --- a/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -8,6 +8,8 @@ import com.axonivy.market.entity.Product; +import java.util.Optional; + @Repository public interface ProductRepository extends MongoRepository { @@ -15,6 +17,8 @@ public interface ProductRepository extends MongoRepository { Product findByLogoUrl(String logoUrl); + Optional findById(String productId); + @Query("{'marketDirectory': {$regex : ?0, $options: 'i'}}") Product findByMarketDirectoryRegex(String search); diff --git a/src/main/java/com/axonivy/market/service/VersionService.java b/src/main/java/com/axonivy/market/service/VersionService.java new file mode 100644 index 000000000..214e7b669 --- /dev/null +++ b/src/main/java/com/axonivy/market/service/VersionService.java @@ -0,0 +1,17 @@ +package com.axonivy.market.service; + +import com.axonivy.market.model.MavenArtifactVersionModel; + +import java.util.List; + +public interface VersionService { + + List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion); + + List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactId); + + String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactId); + + List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, + String designerVersion); +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java new file mode 100644 index 000000000..f1aeaba06 --- /dev/null +++ b/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -0,0 +1,350 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Product; +import com.axonivy.market.github.model.ArchivedArtifact; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.entity.MavenArtifactModel; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.VersionService; +import com.axonivy.market.comparator.ArchivedArtifactsComparator; +import com.axonivy.market.comparator.LatestVersionComparator; +import com.axonivy.market.utils.XmlReaderUtils; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.github.GHContent; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Log4j2 +@Service +@Getter +public class VersionServiceImpl implements VersionService { + + private final GHAxonIvyProductRepoService gitHubService; + private final MavenArtifactVersionRepository mavenArtifactVersionRepository; + private final ProductRepository productRepository; + @Getter + private String repoName; + private Map> archivedArtifactsMap; + private List artifactsFromMeta; + private MavenArtifactVersion proceedDataCache; + private MavenArtifact metaProductArtifact; + private final LatestVersionComparator latestVersionComparator = new LatestVersionComparator(); + @Getter + private String productJsonFilePath; + private String productId; + + public VersionServiceImpl(GHAxonIvyProductRepoService gitHubService, + MavenArtifactVersionRepository mavenArtifactVersionRepository, ProductRepository productRepository) { + this.gitHubService = gitHubService; + this.mavenArtifactVersionRepository = mavenArtifactVersionRepository; + this.productRepository = productRepository; + + } + + private void resetData() { + repoName = null; + archivedArtifactsMap = new HashMap<>(); + artifactsFromMeta = Collections.emptyList(); + proceedDataCache = null; + metaProductArtifact = null; + productJsonFilePath = null; + productId = null; + + } + + public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, + String designerVersion) { + List results = new ArrayList<>(); + resetData(); + + this.productId = productId; + artifactsFromMeta = getProductMetaArtifacts(productId); + List versionsToDisplay = getVersionsToDisplay(isShowDevVersion, designerVersion); + proceedDataCache = mavenArtifactVersionRepository.findById(productId) + .orElse(new MavenArtifactVersion(productId)); + metaProductArtifact = artifactsFromMeta.stream() + .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)) + .findAny().orElse(new MavenArtifact()); + + sanitizeMetaArtifactBeforeHandle(); + + boolean isNewVersionDetected = handleArtifactForVersionToDisplay(versionsToDisplay, results); + if (isNewVersionDetected) { + mavenArtifactVersionRepository.save(proceedDataCache); + } + return results; + } + + public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, + List result) { + boolean isNewVersionDetected = false; + for (String version : versionsToDisplay) { + List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); + List productArtifactModels = proceedDataCache.getProductArtifactWithVersionReleased() + .get(version); + if (productArtifactModels == null) { + isNewVersionDetected = true; + productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); + } + artifactsInVersion.addAll(productArtifactModels); + result.add(new MavenArtifactVersionModel(version, artifactsInVersion.stream().distinct().toList())); + } + return isNewVersionDetected; + } + + public List updateArtifactsInVersionWithProductArtifact(String version) { + List productArtifactModels = convertMavenArtifactsToModels(getProductJsonByVersion(version), + version); + proceedDataCache.getVersions().add(version); + proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); + return productArtifactModels; + } + + public List getProductMetaArtifacts(String productId) { + Product productInfo = productRepository.findById(productId).orElse(new Product()); + String fullRepoName = productInfo.getRepositoryName(); + if (StringUtils.isNotEmpty(fullRepoName)) { + repoName = getRepoNameFromMarketRepo(fullRepoName); + } + return Optional.ofNullable(productInfo.getArtifacts()).orElse(new ArrayList<>()); + } + + public void sanitizeMetaArtifactBeforeHandle() { + artifactsFromMeta.remove(metaProductArtifact); + artifactsFromMeta.forEach(artifact -> { + List archivedArtifacts = new ArrayList<>( + Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).stream() + .sorted(new ArchivedArtifactsComparator()).toList()); + Collections.reverse(archivedArtifacts); + archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); + }); + } + + @Override + public List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion) { + List versions = getVersionsFromMavenArtifacts(); + Stream versionStream = versions.stream(); + if (BooleanUtils.isTrue(isShowDevVersion)) { + return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) + .sorted(new LatestVersionComparator()).toList(); + } + if (StringUtils.isNotBlank(designerVersion)) { + return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); + } + return versions.stream().filter(this::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); + } + + public List getVersionsFromMavenArtifacts() { + Set versions = new HashSet<>(); + for (MavenArtifact artifact : artifactsFromMeta) { + versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), + artifact.getArtifactId())); + Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()) + .forEach(archivedArtifact -> versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), + archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()))); + } + List versionList = new ArrayList<>(versions); + versionList.sort(new LatestVersionComparator()); + return versionList; + } + + @Override + public List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactID) { + List versions = new ArrayList<>(); + String baseUrl = buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactID); + if (StringUtils.isNotBlank(baseUrl)) { + versions.addAll(XmlReaderUtils.readXMLFromUrl(baseUrl)); + } + return versions; + } + + @Override + public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactID) { + if (StringUtils.isAnyBlank(groupId, artifactID)) { + return StringUtils.EMPTY; + } + repoUrl = Optional.ofNullable(repoUrl).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + groupId = groupId.replace(MavenConstants.DOT_SEPARATOR, MavenConstants.GROUP_ID_URL_SEPARATOR); + return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); + } + + public String getBugfixVersion(String version) { + + if (isSnapshotVersion(version)) { + version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); + } else if (isSprintVersion(version)) { + version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; + } + String[] segments = version.split("\\."); + if (segments.length >= 3) { + segments[2] = segments[2].split(MavenConstants.ARTIFACT_ID_SEPARATOR)[0]; + return segments[0] + MavenConstants.DOT_SEPARATOR + segments[1] + MavenConstants.DOT_SEPARATOR + + segments[2]; + } + return version; + } + + public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { + if (isReleasedVersion(version)) { + return true; + } + String bugfixVersion; + if (isSnapshotVersion(version)) { + bugfixVersion = getBugfixVersion( + version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); + } else { + bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); + } + return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) + && getBugfixVersion(currentVersion).equals(bugfixVersion)); + } + + public boolean isSnapshotVersion(String version) { + return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); + } + + public boolean isSprintVersion(String version) { + return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); + } + + public boolean isReleasedVersion(String version) { + return !(isSprintVersion(version) || isSnapshotVersion(version)); + } + + public boolean isMatchWithDesignerVersion(String version, String designerVersion) { + return isReleasedVersion(version) && version.startsWith(designerVersion); + } + + public List getProductJsonByVersion(String version) { + List result = new ArrayList<>(); + String versionTag = buildProductJsonFilePath(version); + try { + GHContent productJsonContent = gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, + versionTag); + if (Objects.isNull(productJsonContent)) { + return result; + } + result = gitHubService.convertProductJsonToMavenProductInfo(productJsonContent); + } catch (IOException e) { + log.warn("Can not get the product.json from repo {} by path in {} version {}", repoName, + productJsonFilePath, versionTag); + } + return result; + } + + public String buildProductJsonFilePath(String version) { + String versionTag = "v" + version; + String pathToProductJsonFileFromTagContent = metaProductArtifact.getArtifactId(); + switch (productId) { + case NonStandardProductPackageConstants.PORTAL: + pathToProductJsonFileFromTagContent = "AxonIvyPortal/portal-product"; + versionTag = version; + break; + case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: + pathToProductJsonFileFromTagContent = "connectivity/connectivity-demos-product"; + break; + case NonStandardProductPackageConstants.ERROR_HANDLING: + pathToProductJsonFileFromTagContent = "error-handling/error-handling-demos-product"; + break; + case NonStandardProductPackageConstants.WORKFLOW_DEMO: + pathToProductJsonFileFromTagContent = "workflow/workflow-demos-product"; + break; + case NonStandardProductPackageConstants.MICROSOFT_365: + pathToProductJsonFileFromTagContent = "msgraph-connector-product/products/msgraph-connector"; + break; + case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: + pathToProductJsonFileFromTagContent = "html-dialog/html-dialog-demos-product"; + break; + case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: + pathToProductJsonFileFromTagContent = "rule-engine/rule-engine-demos-product"; + break; + default: + break; + } + productJsonFilePath = String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, + pathToProductJsonFileFromTagContent); + return versionTag; + } + + public MavenArtifactModel convertMavenArtifactToModel(MavenArtifact artifact, String version) { + String artifactName = artifact.getName(); + if (StringUtils.isBlank(artifactName)) { + artifactName = convertArtifactIdToName(artifact.getArtifactId()); + } + artifact.setType(Optional.ofNullable(artifact.getType()).orElse("iar")); + artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); + return new MavenArtifactModel(artifactName, buildDownloadUrlFromArtifactAndVersion(artifact, version), + artifact.getIsProductArtifact()); + } + + public List convertMavenArtifactsToModels(List artifacts, String version) { + List results = new ArrayList<>(); + if (!CollectionUtils.isEmpty(artifacts)) { + for (MavenArtifact artifact : artifacts) { + MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); + results.add(mavenArtifactModel); + } + } + return results; + } + + public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, String version) { + String groupIdByVersion = artifact.getGroupId(); + String artifactIdByVersion = artifact.getArtifactId(); + String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion( + artifact.getArtifactId(), version); + + if (Objects.nonNull(archivedArtifactBestMatchVersion)) { + groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); + artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); + } + groupIdByVersion = groupIdByVersion.replace(MavenConstants.DOT_SEPARATOR, + MavenConstants.GROUP_ID_URL_SEPARATOR); + return String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, repoUrl, groupIdByVersion, + artifactIdByVersion, version, artifactIdByVersion, version, artifact.getType()); + } + + public ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String artifactId, String version) { + List archivedArtifacts = archivedArtifactsMap.get(artifactId); + + if (CollectionUtils.isEmpty(archivedArtifacts)) { + return null; + } + for (ArchivedArtifact archivedArtifact : archivedArtifacts) { + if (latestVersionComparator.compare(archivedArtifact.getLastVersion(), version) <= 0) { + return archivedArtifact; + } + } + return null; + } + + public String convertArtifactIdToName(String artifactId) { + if (StringUtils.isBlank(artifactId)) { + return StringUtils.EMPTY; + } + return Arrays.stream(artifactId.split(MavenConstants.ARTIFACT_ID_SEPARATOR)) + .map(part -> part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase()) + .collect(Collectors.joining(MavenConstants.ARTIFACT_NAME_SEPARATOR)); + } + + public String getRepoNameFromMarketRepo(String fullRepoName) { + String[] repoNamePart = fullRepoName.split("/"); + return repoNamePart[repoNamePart.length - 1]; + } +} \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java b/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java new file mode 100644 index 000000000..8f7f6a6bc --- /dev/null +++ b/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java @@ -0,0 +1,59 @@ +package com.axonivy.market.utils; + +import com.axonivy.market.constants.MavenConstants; +import lombok.extern.log4j.Log4j2; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Log4j2 +public class XmlReaderUtils { + private static final RestTemplate restTemplate = new RestTemplate(); + + private XmlReaderUtils() { + } + + public static List readXMLFromUrl(String url) { + List versions = new ArrayList<>(); + try { + String xmlData = restTemplate.getForObject(url, String.class); + extractVersions(xmlData, versions); + } catch (HttpClientErrorException e) { + log.error(e.getMessage()); + } + return versions; + } + + public static void extractVersions(String xmlData, List versions) { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(xmlData))); + + XPath xpath = XPathFactory.newInstance().newXPath(); + XPathExpression expr = xpath.compile(MavenConstants.VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE); + + Object result = expr.evaluate(document, XPathConstants.NODESET); + NodeList versionNodes = (NodeList) result; + + for (int i = 0; i < versionNodes.getLength(); i++) { + versions.add(Optional.ofNullable(versionNodes.item(i)).map(Node::getTextContent).orElse(null)); + } + } catch (Exception e) { + log.error(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/controller/AppControllerTest.java b/src/test/java/com/axonivy/market/controller/AppControllerTest.java index b46637a6e..104006d9d 100644 --- a/src/test/java/com/axonivy/market/controller/AppControllerTest.java +++ b/src/test/java/com/axonivy/market/controller/AppControllerTest.java @@ -22,5 +22,4 @@ void testRoot() throws Exception { assertTrue(response.hasBody()); assertTrue(response.getBody().getMessageDetails().contains("/swagger-ui/index.html")); } - -} +} \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 68e846755..a2c6f3ec7 100644 --- a/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -102,4 +102,4 @@ private Product createProductMock() { mockProduct.setTags(List.of("AI")); return mockProduct; } -} +} \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index b8babd511..1779befd8 100644 --- a/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -2,11 +2,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.service.VersionService; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.List; +import java.util.Objects; @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { @@ -14,10 +23,22 @@ class ProductDetailsControllerTest { @InjectMocks private ProductDetailsController productDetailsController; + @Mock + VersionService versionService; + @Test void testFindProduct() { var result = productDetailsController.findProduct("", ""); assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); } -} + @Test + void testFindProductVersionsById(){ + List models = List.of(new MavenArtifactVersionModel()); + Mockito.when(versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())).thenReturn(models); + ResponseEntity> result = productDetailsController.findProductVersionsById("protal", true, "10.0.1"); + Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()); + Assertions.assertEquals(1, Objects.requireNonNull(result.getBody()).size()); + Assertions.assertEquals(models, result.getBody()); + } +} \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index 7c8099b91..96d27d5bf 100644 --- a/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -10,6 +10,8 @@ import java.io.IOException; import java.io.InputStream; +import com.axonivy.market.github.model.Meta; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; @@ -20,31 +22,54 @@ @ExtendWith(MockitoExtension.class) class ProductFactoryTest { - private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; - - @Test - void testMappingByGHContent() throws IOException { - Product product = new Product(); - GHContent mockContent = mock(GHContent.class); - when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); - InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); - when(mockContent.read()).thenReturn(inputStream); - var result = ProductFactory.mappingByGHContent(product, mockContent); - assertNotEquals(null, result); - assertEquals("Amazon Comprehend", result.getName()); - } - - @Test - void testMappingLogo() throws IOException { - Product product = new Product(); - GHContent content = mock(GHContent.class); - when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); - var result = ProductFactory.mappingByGHContent(product, content); - assertNotEquals(null, result); - - when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); - when(content.getDownloadUrl()).thenReturn(DUMMY_LOGO_URL); - result = ProductFactory.mappingByGHContent(product, content); - assertNotEquals(null, result); - } + private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; + + @Test + void testMappingByGHContent() throws IOException { + Product product = new Product(); + GHContent mockContent = mock(GHContent.class); + var result = ProductFactory.mappingByGHContent(product, null); + assertEquals(product, result); + when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); + InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); + when(mockContent.read()).thenReturn(inputStream); + result = ProductFactory.mappingByGHContent(product, mockContent); + assertNotEquals(null, result); + assertEquals("Amazon Comprehend", result.getName()); + } + + @Test + void testMappingLogo() throws IOException { + Product product = new Product(); + GHContent content = mock(GHContent.class); + when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + var result = ProductFactory.mappingByGHContent(product, content); + assertNotEquals(null, result); + + when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + when(content.getDownloadUrl()).thenReturn(DUMMY_LOGO_URL); + result = ProductFactory.mappingByGHContent(product, content); + assertNotEquals(null, result); + } + + @Test + void testExtractSourceUrl() throws IOException { + Product product = new Product(); + Meta meta = new Meta(); + ProductFactory.extractSourceUrl(product, meta); + Assertions.assertNull(product.getRepositoryName()); + Assertions.assertNull(product.getSourceUrl()); + + String sourceUrl = "https://github.com/axonivy-market/alfresco-connector"; + meta.setSourceUrl(sourceUrl); + ProductFactory.extractSourceUrl(product, meta); + Assertions.assertEquals("axonivy-market/alfresco-connector", product.getRepositoryName()); + Assertions.assertEquals(sourceUrl, product.getSourceUrl()); + + sourceUrl = "portal"; + meta.setSourceUrl(sourceUrl); + ProductFactory.extractSourceUrl(product, meta); + Assertions.assertEquals(sourceUrl, product.getRepositoryName()); + Assertions.assertEquals(sourceUrl, product.getSourceUrl()); + } } diff --git a/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java b/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java new file mode 100644 index 000000000..28293329d --- /dev/null +++ b/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java @@ -0,0 +1,242 @@ +package com.axonivy.market.github.service; + +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GHAxonIvyProductRepoServiceImplTest { + + private static final String DUMMY_TAG = "v1.0.0"; + + @Mock + PagedIterable listTags; + + @Mock + GHRepository ghRepository; + + @Mock + GitHubService githubService; + + @Mock + GHOrganization organization; + + @Mock + JsonNode dataNode; + + @Mock + JsonNode childNode; + + @Mock + GHContent content = new GHContent(); + + @InjectMocks + @Spy + private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; + + void setup() throws IOException { + var mockGHOrganization = mock(GHOrganization.class); + when(githubService.getOrganization(any())).thenReturn(mockGHOrganization); + when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); + } + + @Test + void testAllTagsFromRepoName() throws IOException { + setup(); + var mockTag = mock(GHTag.class); + when(mockTag.getName()).thenReturn(DUMMY_TAG); + when(listTags.toList()).thenReturn(List.of(mockTag)); + when(ghRepository.listTags()).thenReturn(listTags); + var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); + assertEquals(1, result.size()); + assertEquals(DUMMY_TAG, result.get(0).getName()); + } + + @Test + void testContentFromGHRepoAndTag() throws IOException { + setup(); + var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertNull(result); + when(axonivyProductRepoServiceImpl.getOrganization()).thenThrow(IOException.class); + result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertNull(result); + } + + @Test + void testExtractMavenArtifactFromJsonNode() { + List artifacts = new ArrayList<>(); + boolean isDependency = true; + String nodeName = ProductJsonConstants.DEPENDENCIES; + + createListNodeForDataNoteByName(nodeName); + MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, + isDependency); + + axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + + assertEquals(1, artifacts.size()); + assertSame(mockArtifact, artifacts.get(0)); + + isDependency = false; + nodeName = ProductJsonConstants.PROJECTS; + createListNodeForDataNoteByName(nodeName); + + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, + isDependency); + + axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + + assertEquals(2, artifacts.size()); + assertSame(mockArtifact, artifacts.get(1)); + } + + private void createListNodeForDataNoteByName(String nodeName) { + JsonNode sectionNode = Mockito.mock(JsonNode.class); + Iterator iterator = Mockito.mock(Iterator.class); + Mockito.when(dataNode.path(nodeName)).thenReturn(sectionNode); + Mockito.when(sectionNode.iterator()).thenReturn(iterator); + Mockito.when(iterator.hasNext()).thenReturn(true, false); + Mockito.when(iterator.next()).thenReturn(childNode); + } + + @Test + void testCreateArtifactFromJsonNode() { + String repoUrl = "http://example.com/repo"; + boolean isDependency = true; + String groupId = "com.example"; + String artifactId = "example-artifact"; + String type = "jar"; + + JsonNode groupIdNode = Mockito.mock(JsonNode.class); + JsonNode artifactIdNode = Mockito.mock(JsonNode.class); + JsonNode typeNode = Mockito.mock(JsonNode.class); + Mockito.when(groupIdNode.asText()).thenReturn(groupId); + Mockito.when(artifactIdNode.asText()).thenReturn(artifactId); + Mockito.when(typeNode.asText()).thenReturn(type); + Mockito.when(dataNode.path(ProductJsonConstants.GROUP_ID)).thenReturn(groupIdNode); + Mockito.when(dataNode.path(ProductJsonConstants.ARTIFACT_ID)).thenReturn(artifactIdNode); + Mockito.when(dataNode.path(ProductJsonConstants.TYPE)).thenReturn(typeNode); + + MavenArtifact artifact = axonivyProductRepoServiceImpl.createArtifactFromJsonNode(dataNode, repoUrl, + isDependency); + + assertEquals(repoUrl, artifact.getRepoUrl()); + assertTrue(artifact.getIsDependency()); + assertEquals(groupId, artifact.getGroupId()); + assertEquals(artifactId, artifact.getArtifactId()); + assertEquals(type, artifact.getType()); + assertTrue(artifact.getIsProductArtifact()); + } + + @Test + void testConvertProductJsonToMavenProductInfo() throws IOException { + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(null).size()); + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + + InputStream inputStream = getMockInputStream(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(2, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + inputStream = getMockInputStreamWithOutProjectAndDependency(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + } + + private static InputStream getMockInputStream() { + String jsonContent = """ + { + "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", + "installers": [ + { + "id": "maven-import", + "data": { + "projects": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic-demo", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + } + ] + } + """; + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + private static InputStream getMockInputStreamWithOutProjectAndDependency() { + String jsonContent = "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" + + " \"repositories\": [\n" + " {\n" + + " \"url\": \"http://example.com/repo\"\n" + " }\n" + " ]\n" + " }\n" + + " }\n" + " ]\n" + "}"; + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void testExtractedContentStream() { + assertNull(axonivyProductRepoServiceImpl.extractedContentStream(null)); + assertNull(axonivyProductRepoServiceImpl.extractedContentStream(content)); + } + + @Test + void testGetOrganization() throws IOException { + Mockito.when(githubService.getOrganization(Mockito.anyString())).thenReturn(organization); + assertEquals(organization, axonivyProductRepoServiceImpl.getOrganization()); + assertEquals(organization, axonivyProductRepoServiceImpl.getOrganization()); + } +} diff --git a/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java deleted file mode 100644 index d5d8ba716..000000000 --- a/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.axonivy.market.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHTag; -import org.kohsuke.github.PagedIterable; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; - -@ExtendWith(MockitoExtension.class) -class GHAxonIvyProductRepoServiceImplTest { - - private static final String DUMMY_TAG = "v1.0.0"; - - @Mock - PagedIterable listTags; - - @Mock - GHRepository ghRepository; - - @Mock - GitHubService gitHubService; - - @InjectMocks - private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; - - @BeforeEach - void setup() throws IOException { - var mockGHOrganization = mock(GHOrganization.class); - when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); - when(gitHubService.getOrganization(any())).thenReturn(mockGHOrganization); - } - - @Test - void testAllTagsFromRepoName() throws IOException { - var mockTag = mock(GHTag.class); - when(mockTag.getName()).thenReturn(DUMMY_TAG); - when(listTags.toList()).thenReturn(List.of(mockTag)); - when(ghRepository.listTags()).thenReturn(listTags); - var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); - assertEquals(1, result.size()); - assertEquals(DUMMY_TAG, result.get(0).getName()); - } - - @Test - void testContentFromGHRepoAndTag() { - var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); - assertEquals(null, result); - } -} diff --git a/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index 159c43683..ea3bd0fe7 100644 --- a/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -7,6 +7,7 @@ import java.io.IOException; +import com.axonivy.market.github.service.impl.GitHubServiceImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; @@ -16,8 +17,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.github.service.impl.GitHubServiceImpl; - @ExtendWith(MockitoExtension.class) class GitHubServiceImplTest { private static final String DUMMY_API_URL = "https://api.github.com"; diff --git a/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java new file mode 100644 index 000000000..932b7a864 --- /dev/null +++ b/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java @@ -0,0 +1,565 @@ +package com.axonivy.market.service; + +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.entity.MavenArtifactModel; +import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Product; +import com.axonivy.market.github.model.ArchivedArtifact; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.impl.VersionServiceImpl; +import com.axonivy.market.utils.XmlReaderUtils; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Fail; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.*; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.util.*; + +@Log4j2 +@ExtendWith(MockitoExtension.class) +class VersionServiceImplTest { + private String repoName; + private Map> archivedArtifactsMap; + private List artifactsFromMeta; + private MavenArtifactVersion proceedDataCache; + private MavenArtifact metaProductArtifact; + @Spy + @InjectMocks + private VersionServiceImpl versionService; + + @Mock + private GHAxonIvyProductRepoService gitHubService; + + @Mock + private MavenArtifactVersionRepository mavenArtifactVersionRepository; + + @Mock + private ProductRepository productRepository; + + @BeforeEach() + void prepareBeforeTest() { + archivedArtifactsMap = new HashMap<>(); + artifactsFromMeta = new ArrayList<>(); + metaProductArtifact = new MavenArtifact(); + proceedDataCache = new MavenArtifactVersion(); + repoName = StringUtils.EMPTY; + ReflectionTestUtils.setField(versionService, "archivedArtifactsMap", archivedArtifactsMap); + ReflectionTestUtils.setField(versionService, "artifactsFromMeta", artifactsFromMeta); + ReflectionTestUtils.setField(versionService, "proceedDataCache", proceedDataCache); + ReflectionTestUtils.setField(versionService, "metaProductArtifact", metaProductArtifact); + } + + private void setUpArtifactFromMeta() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + metaProductArtifact.setGroupId(groupId); + metaProductArtifact.setArtifactId(artifactId); + metaProductArtifact.setIsProductArtifact(true); + MavenArtifact additionalMavenArtifact = new MavenArtifact(repoUrl, "", groupId, artifactId, "", null, null, + null); + artifactsFromMeta.add(metaProductArtifact); + artifactsFromMeta.add(additionalMavenArtifact); + } + + @Test + void testGetArtifactsAndVersionToDisplay() { + String productId = "adobe-acrobat-sign-connector"; + String targetVersion = "10.0.10"; + setUpArtifactFromMeta(); + when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); + when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())) + .thenReturn(List.of(targetVersion)); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); + ArrayList artifactsInVersion = new ArrayList<>(); + artifactsInVersion.add(new MavenArtifactModel()); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(artifactsInVersion); + Assertions.assertEquals(1, + versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + + MavenArtifactVersion proceededData = new MavenArtifactVersion(); + proceededData.getProductArtifactWithVersionReleased().put(targetVersion, new ArrayList<>()); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); + Assertions.assertEquals(1, + versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + } + + @Test + void testHandleArtifactForVersionToDisplay() { + String newVersionDetected = "10.0.10"; + List result = new ArrayList<>(); + List versionsToDisplay = List.of(newVersionDetected); + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + Assertions.assertTrue(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(newVersionDetected, result.get(0).getVersion()); + Assertions.assertEquals(0, result.get(0).getArtifactsByVersion().size()); + + result = new ArrayList<>(); + ArrayList artifactsInVersion = new ArrayList<>(); + artifactsInVersion.add(new MavenArtifactModel()); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(artifactsInVersion); + Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); + } + + @Test + void testGetProductMetaArtifacts() { + Product product = new Product(); + MavenArtifact artifact1 = new MavenArtifact(); + MavenArtifact artifact2 = new MavenArtifact(); + List artifacts = List.of(artifact1, artifact2); + product.setArtifacts(artifacts); + when(productRepository.findById(Mockito.anyString())).thenReturn(Optional.of(product)); + List result = versionService.getProductMetaArtifacts("portal"); + Assertions.assertEquals(artifacts, result); + Assertions.assertNull(versionService.getRepoName()); + + product.setRepositoryName("/market/portal"); + versionService.getProductMetaArtifacts("portal"); + Assertions.assertEquals("portal", versionService.getRepoName()); + } + + @Test + void testUpdateArtifactsInVersionWithProductArtifact() { + String version = "10.0.10"; + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + MavenArtifactModel artifactModel = new MavenArtifactModel(); + List mockMavenArtifactModels = List.of(artifactModel); + when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(mockMavenArtifactModels); + Assertions.assertEquals(mockMavenArtifactModels, + versionService.updateArtifactsInVersionWithProductArtifact(version)); + Assertions.assertEquals(1, proceedDataCache.getVersions().size()); + Assertions.assertEquals(1, proceedDataCache.getProductArtifactWithVersionReleased().size()); + Assertions.assertEquals(version, proceedDataCache.getVersions().get(0)); + } + + @Test + void testSanitizeMetaArtifactBeforeHandle() { + setUpArtifactFromMeta(); + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String archivedArtifactId1 = "adobe-acrobat-sign-connector"; + String archivedArtifactId2 = "adobe-acrobat-sign-connector"; + ArchivedArtifact archivedArtifact1 = new ArchivedArtifact("10.0.10", groupId, archivedArtifactId1); + ArchivedArtifact archivedArtifact2 = new ArchivedArtifact("10.0.20", groupId, archivedArtifactId2); + artifactsFromMeta.get(1).setArchivedArtifacts(List.of(archivedArtifact2, archivedArtifact1)); + + versionService.sanitizeMetaArtifactBeforeHandle(); + String artifactId = "adobe-acrobat-sign-connector"; + + Assertions.assertEquals(1, artifactsFromMeta.size()); + Assertions.assertEquals(1, archivedArtifactsMap.size()); + Assertions.assertEquals(2, archivedArtifactsMap.get(artifactId).size()); + Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); + } + + @Test + void testGetVersionsToDisplay() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + versionFromArtifact.add("10.0.3-SNAPSHOT"); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)) + .thenReturn(versionFromArtifact); + Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(true, null)); + Assertions.assertEquals(List.of("10.0.5"), versionService.getVersionsToDisplay(null, "10.0.5")); + versionFromArtifact.remove("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(null, null)); + } + + @Test + void getVersionsFromMavenArtifacts() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + String archivedArtifactId = "adobe-sign-connector"; + artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)) + .thenReturn(versionFromArtifact); + Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); + + List archivedArtifacts = List.of(new ArchivedArtifact("10.0.9", groupId, archivedArtifactId)); + ArrayList versionFromArchivedArtifact = new ArrayList<>(); + versionFromArchivedArtifact.add("10.0.3"); + versionFromArchivedArtifact.add("10.0.2"); + versionFromArchivedArtifact.add("10.0.1"); + artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)) + .thenReturn(versionFromArchivedArtifact); + versionFromArtifact.addAll(versionFromArchivedArtifact); + Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); + } + + @Test + void testGetVersionsFromArtifactDetails() { + + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.16"); + versionFromArtifact.add("10.0.18"); + versionFromArtifact.add("10.0.19"); + versionFromArtifact.add("10.0.20"); + versionFromArtifact.add("10.0.21"); + + try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { + xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); + } + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), + versionFromArtifact); + } + + @Test + void testBuildMavenMetadataUrlFromArtifact() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + String metadataUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; + Assertions.assertEquals(StringUtils.EMPTY, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); + Assertions.assertEquals(StringUtils.EMPTY, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), StringUtils.EMPTY); + Assertions.assertEquals(metadataUrl, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); + } + + @Test + void testIsReleasedVersionOrUnReleaseDevVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + String unreleasedSprintVersion = "10.0.21-m1235"; + List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); + Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); + Assertions + .assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); + } + + @Test + void testGetBugfixVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(releasedVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(snapshotVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(sprintVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(minorSprintVersion)); + } + + @Test + void testIsSnapshotVersion() { + String targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertTrue(versionService.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21-m1234"; + Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); + } + + @Test + void testIsSprintVersion() { + String targetVersion = "10.0.21-m1234"; + Assertions.assertTrue(versionService.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); + } + + @Test + void testIsReleasedVersion() { + String targetVersion = "10.0.21"; + Assertions.assertTrue(versionService.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-m1231"; + Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); + } + + @Test + void testIsMatchWithDesignerVersion() { + String designerVersion = "10.0.21"; + String targetVersion = "10.0.21.2"; + Assertions.assertTrue(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.19"; + Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + } + + @Test + void testGetProductJsonByVersion() { + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector.adobe.acrobat"; + GHContent mockContent = mock(GHContent.class); + repoName = "adobe-acrobat-sign-connector"; + ReflectionTestUtils.setField(versionService, "repoName", repoName); + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + MavenArtifact productArtifact = new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, + targetArtifactId, "iar", null, true, null); + + metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); + metaProductArtifact.setGroupId(targetGroupId); + metaProductArtifact.setArtifactId(targetArtifactId); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(null); + Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); + + metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockContent); + + try { + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); + Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); + + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)) + .thenThrow(new IOException("Mock IO Exception")); + Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); + } catch (IOException e) { + Fail.fail("Mock setup should not throw an exception"); + } + } + + @Test + void testConvertMavenArtifactToModel() { + String downloadUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; + String artifactName = "Adobe Acrobat Sign Connector (iar)"; + + MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null); + + // Assert case handle artifact without name + MavenArtifactModel result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); + MavenArtifactModel expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); + Assertions.assertEquals(expectedResult.getName(), result.getName()); + Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); + + // Assert case handle artifact with name + artifactName = "Adobe Connector"; + String expectedArtifactName = "Adobe Connector (iar)"; + targetArtifact.setName(artifactName); + result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); + expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); + Assertions.assertEquals(expectedArtifactName, result.getName()); + Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); + } + + @Test + void testConvertMavenArtifactsToModels() { + // Assert case param is empty + List result = versionService.convertMavenArtifactsToModels(Collections.emptyList(), + "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is null + result = versionService.convertMavenArtifactsToModels(null, "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is a list with existed element + MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null); + result = versionService.convertMavenArtifactsToModels(List.of(targetArtifact), "10.0.21"); + Assertions.assertEquals(1, result.size()); + } + + @Test + void testBuildDownloadUrlFromArtifactAndVersion() { + // Set up artifact for testing + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector"; + MavenArtifact targetArtifact = new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, + null); + String targetVersion = "10.0.10"; + + // Assert case without archived artifact + String expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, + MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, "com/axonivy/connector", targetArtifactId, targetVersion, + targetArtifactId, targetVersion, "iar"); + String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact not match & use custom repo + ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", + "adobe-connector"); + ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", + "com.axonivy.adobe.sign.connector", "adobe-sign-connector"); + archivedArtifactsMap.put(targetArtifactId, + List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); + String customRepoUrl = "https://nexus.axonivy.com"; + targetArtifact.setRepoUrl(customRepoUrl); + result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); + expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, + "com/axonivy/connector", targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact got matching archived artifact & use custom file + // type + String customType = "zip"; + targetArtifact.setType(customType); + targetVersion = "10.0.9"; + result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, "10.0.9"); + expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, + "com/axonivy/adobe/connector", "adobe-connector", targetVersion, "adobe-connector", targetVersion, + customType); + Assertions.assertEquals(expectedResult, result); + } + + @Test + void testFindArchivedArtifactInfoBestMatchWithVersion() { + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetVersion = "10.0.10"; + ArchivedArtifact result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, + targetVersion); + Assertions.assertNull(result); + + // Assert case with target version higher than all of latest version from + // archived artifact list + ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.connector", + "adobe-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.connector", + "adobe-acrobat-sign-connector"); + List archivedArtifacts = new ArrayList<>(); + archivedArtifacts.add(adobeArchivedArtifactVersion8); + archivedArtifacts.add(adobeArchivedArtifactVersion9); + archivedArtifactsMap.put(targetArtifactId, archivedArtifacts); + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertNull(result); + + // Assert case with target version less than all of latest version from archived + // artifact list + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, "10.0.7"); + Assertions.assertEquals(adobeArchivedArtifactVersion8, result); + + // Assert case with target version is in range of archived artifact list + ArchivedArtifact adobeArchivedArtifactVersion10 = new ArchivedArtifact("10.0.10", "com.axonivy.connector", + "adobe-sign-connector"); + + archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertEquals(adobeArchivedArtifactVersion10, result); + } + + @Test + void testConvertArtifactIdToName() { + String defaultArtifactId = "adobe-acrobat-sign-connector"; + String result = versionService.convertArtifactIdToName(defaultArtifactId); + Assertions.assertEquals("Adobe Acrobat Sign Connector", result); + + result = versionService.convertArtifactIdToName(null); + Assertions.assertEquals(StringUtils.EMPTY, result); + + result = versionService.convertArtifactIdToName(StringUtils.EMPTY); + Assertions.assertEquals(StringUtils.EMPTY, result); + + result = versionService.convertArtifactIdToName(" "); + Assertions.assertEquals(StringUtils.EMPTY, result); + } + + @Test + void testGetRepoNameFromMarketRepo() { + String defaultRepositoryName = "market/adobe-acrobat-connector"; + String expectedRepoName = "adobe-acrobat-connector"; + String result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + + defaultRepositoryName = "market/utils/adobe-acrobat-connector"; + result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + + defaultRepositoryName = "adobe-acrobat-connector"; + result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + } + + @Test + void testBuildProductJsonFilePath() { + String version = "10.0.1"; + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + Assertions.assertEquals("v10.0.1", versionService.buildProductJsonFilePath(version)); + + ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.PORTAL); + Assertions.assertEquals("10.0.1", versionService.buildProductJsonFilePath(version)); + Assertions.assertEquals("AxonIvyPortal/portal-product/product.json", versionService.getProductJsonFilePath()); + + ReflectionTestUtils.setField(versionService, "productId", + NonStandardProductPackageConstants.CONNECTIVITY_FEATURE); + versionService.buildProductJsonFilePath(version); + Assertions.assertEquals("connectivity/connectivity-demos-product/product.json", + versionService.getProductJsonFilePath()); + + ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.ERROR_HANDLING); + versionService.buildProductJsonFilePath(version); + Assertions.assertEquals("error-handling/error-handling-demos-product/product.json", + versionService.getProductJsonFilePath()); + + ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.WORKFLOW_DEMO); + versionService.buildProductJsonFilePath(version); + Assertions.assertEquals("workflow/workflow-demos-product/product.json", + versionService.getProductJsonFilePath()); + + ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.MICROSOFT_365); + versionService.buildProductJsonFilePath(version); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-connector/product.json", + versionService.getProductJsonFilePath()); + + ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.HTML_DIALOG_DEMO); + versionService.buildProductJsonFilePath(version); + versionService.buildProductJsonFilePath(version); + Assertions.assertEquals("html-dialog/html-dialog-demos-product/product.json", + versionService.getProductJsonFilePath()); + + ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.RULE_ENGINE_DEMOS); + versionService.buildProductJsonFilePath(version); + Assertions.assertEquals("rule-engine/rule-engine-demos-product/product.json", + versionService.getProductJsonFilePath()); + } +} \ No newline at end of file diff --git a/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java b/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java new file mode 100644 index 000000000..7245ce561 --- /dev/null +++ b/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java @@ -0,0 +1,21 @@ +package com.axonivy.market.utils; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class XmlReaderUtilsTest { + + @Test + void testExtractVersions() { + List versions = Collections.emptyList(); + XmlReaderUtils.extractVersions(StringUtils.EMPTY, versions); + Assertions.assertTrue(versions.isEmpty()); + } +} \ No newline at end of file From 9dc6a5f5834ba8d093ec6e36a2ccc594c627b27d Mon Sep 17 00:00:00 2001 From: Dinh Nguyen Date: Wed, 3 Jul 2024 18:34:16 +0700 Subject: [PATCH 11/62] feature/Marp-475 Detail pages for new market installation and download --- .../com/axonivy/market/service/impl/VersionServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index f1aeaba06..36988dcea 100644 --- a/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -210,7 +210,7 @@ public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, St } else { bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); } - return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) + return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) && getBugfixVersion(currentVersion).equals(bugfixVersion)); } From 6880dacbe5fc18822049fb99683e3b28db2e14b8 Mon Sep 17 00:00:00 2001 From: Pham Hoang Hung <84316773+phhung-axonivy@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:22:06 +0700 Subject: [PATCH 12/62] MARP-463 Multilingualism for Website - landing page (#22) --- .../assembler/ProductModelAssembler.java | 4 +- .../market/constants/GitHubConstants.java | 11 +-- .../market/controller/ProductController.java | 5 +- .../market/entity/MavenArtifactModel.java | 10 +-- .../market/entity/MavenArtifactVersion.java | 4 +- .../com/axonivy/market/entity/Product.java | 9 +- .../com/axonivy/market/enums/Language.java | 12 +++ .../com/axonivy/market/enums/SortOption.java | 8 +- .../market/factory/ProductFactory.java | 24 ++++- .../com/axonivy/market/github/model/Meta.java | 5 +- .../market/github/service/GitHubService.java | 4 +- .../impl/GHAxonIvyMarketRepoServiceImpl.java | 5 +- .../service/impl/GitHubServiceImpl.java | 8 +- .../axonivy/market/model/DisplayValue.java | 44 +++++++++ .../market/model/MultilingualismValue.java | 17 ++++ .../axonivy/market/model/ProductModel.java | 4 +- .../market/repository/ProductRepository.java | 8 +- .../market/service/ProductService.java | 2 +- .../service/impl/ProductServiceImpl.java | 17 ++-- .../controller/ProductControllerTest.java | 24 +++-- .../market/factory/ProductFactoryTest.java | 90 ++++++++++--------- .../GHAxonIvyMarketRepoServiceImplTest.java | 2 +- .../market/service/GitHubServiceImplTest.java | 6 +- .../service/ProductServiceImplTest.java | 49 +++++----- .../service/VersionServiceImplTest.java | 45 ++++++---- src/test/resources/meta.json | 22 ++++- 26 files changed, 297 insertions(+), 142 deletions(-) create mode 100644 src/main/java/com/axonivy/market/enums/Language.java create mode 100644 src/main/java/com/axonivy/market/model/DisplayValue.java create mode 100644 src/main/java/com/axonivy/market/model/MultilingualismValue.java diff --git a/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java index 6b36a9d07..bd9c948cd 100644 --- a/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java +++ b/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -27,8 +27,8 @@ public ProductModel toModel(Product product) { private ProductModel createResource(ProductModel model, Product product) { model.setId(product.getId()); - model.setName(product.getName()); - model.setShortDescription(product.getShortDescription()); + model.setNames(product.getNames()); + model.setShortDescriptions(product.getShortDescriptions()); model.setType(product.getType()); model.setTags(product.getTags()); model.setLogoUrl(product.getLogoUrl()); diff --git a/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/src/main/java/com/axonivy/market/constants/GitHubConstants.java index c03c69f65..33c84df88 100644 --- a/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -5,8 +5,9 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubConstants { - public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; - public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; - public static final String AXONIVY_MARKETPLACE_PATH = "market"; - public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; -} \ No newline at end of file + public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; + public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; + public static final String AXONIVY_MARKETPLACE_PATH = "market"; + public static final String DEFAULT_BRANCH = "feature/MARP-463-Multilingualism-for-Website"; + public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; +} diff --git a/src/main/java/com/axonivy/market/controller/ProductController.java b/src/main/java/com/axonivy/market/controller/ProductController.java index 434beb202..55d0444ff 100644 --- a/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/src/main/java/com/axonivy/market/controller/ProductController.java @@ -45,8 +45,9 @@ public ProductController(ProductService service, ProductModelAssembler assembler @GetMapping() public ResponseEntity> findProducts( @RequestParam(required = true, name = "type") String type, - @RequestParam(required = false, name = "keyword") String keyword, Pageable pageable) { - Page results = service.findProducts(type, keyword, pageable); + @RequestParam(required = false, name = "keyword") String keyword, + @RequestParam(required = true, name = "language") String language, Pageable pageable) { + Page results = service.findProducts(type, keyword, language, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); } diff --git a/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java index f3f8977d5..6b5328e26 100644 --- a/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java +++ b/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java @@ -1,14 +1,14 @@ package com.axonivy.market.entity; -import com.axonivy.market.github.model.MavenArtifact; +import java.io.Serializable; +import java.util.Objects; + +import org.springframework.data.annotation.Transient; + import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.springframework.data.annotation.Transient; - -import java.io.Serializable; -import java.util.Objects; @AllArgsConstructor @NoArgsConstructor diff --git a/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java b/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java index f92be46fc..8c8820579 100644 --- a/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java +++ b/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java @@ -21,7 +21,9 @@ @NoArgsConstructor @Document(MAVEN_ARTIFACT_VERSION) public class MavenArtifactVersion implements Serializable { - @Id + private static final long serialVersionUID = -6492612804634492078L; + + @Id private String productId; private List versions = new ArrayList<>(); private Map> productArtifactWithVersionReleased = new HashMap<>(); diff --git a/src/main/java/com/axonivy/market/entity/Product.java b/src/main/java/com/axonivy/market/entity/Product.java index f38748c41..9d89381f7 100644 --- a/src/main/java/com/axonivy/market/entity/Product.java +++ b/src/main/java/com/axonivy/market/entity/Product.java @@ -12,6 +12,9 @@ import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; +import com.axonivy.market.model.MultilingualismValue; +import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -28,9 +31,11 @@ public class Product implements Serializable { @Id private String id; private String marketDirectory; - private String name; + @JsonProperty + private MultilingualismValue names; private String version; - private String shortDescription; + @JsonProperty + private MultilingualismValue shortDescriptions; private String logoUrl; private Boolean listed; private String type; diff --git a/src/main/java/com/axonivy/market/enums/Language.java b/src/main/java/com/axonivy/market/enums/Language.java new file mode 100644 index 000000000..f1115c234 --- /dev/null +++ b/src/main/java/com/axonivy/market/enums/Language.java @@ -0,0 +1,12 @@ +package com.axonivy.market.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Language { + EN("en"), DE("de"); + + private String value; +} diff --git a/src/main/java/com/axonivy/market/enums/SortOption.java b/src/main/java/com/axonivy/market/enums/SortOption.java index 308a01bd3..59914ab90 100644 --- a/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/src/main/java/com/axonivy/market/enums/SortOption.java @@ -10,7 +10,7 @@ @Getter @AllArgsConstructor public enum SortOption { - POPULARITY("popularity", "installationCount"), ALPHABETICALLY("alphabetically", "name"), + POPULARITY("popularity", "installationCount"), ALPHABETICALLY("alphabetically", "names"), RECENT("recent", "newestPublishedDate"); private String option; @@ -25,4 +25,10 @@ public static SortOption of(String option) { } throw new InvalidParamException(ErrorCode.PRODUCT_SORT_INVALID, "SortOption: " + option); } + + public String getCode(String language) { + return StringUtils.isNotBlank(language) && ALPHABETICALLY.option.equalsIgnoreCase(option) + ? String.format("%s.%s", ALPHABETICALLY.code, language) + : code; + } } diff --git a/src/main/java/com/axonivy/market/factory/ProductFactory.java b/src/main/java/com/axonivy/market/factory/ProductFactory.java index 8ec75e239..4cd0f6971 100644 --- a/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -6,13 +6,18 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import java.io.IOException; +import java.util.List; import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHContent; +import org.springframework.util.CollectionUtils; import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; import com.axonivy.market.github.model.Meta; import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.model.DisplayValue; +import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; @@ -49,13 +54,13 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent } product.setId(meta.getId()); - product.setName(meta.getName()); + product.setNames(mappingMultilingualismValueByMetaJSONFile(meta.getNames())); product.setMarketDirectory(extractParentDirectory(ghContent)); product.setListed(meta.getListed()); product.setType(meta.getType()); product.setTags(meta.getTags()); product.setVersion(meta.getVersion()); - product.setShortDescription(meta.getDescription()); + product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); product.setVendor(meta.getVendor()); product.setVendorImage(meta.getVendorImage()); product.setVendorUrl(meta.getVendorUrl()); @@ -68,6 +73,21 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent return product; } + private static MultilingualismValue mappingMultilingualismValueByMetaJSONFile(List list) { + MultilingualismValue value = new MultilingualismValue(); + if (!CollectionUtils.isEmpty(list)) { + for (DisplayValue name : list) { + if (Language.EN.getValue().equalsIgnoreCase(name.getLocale())) { + value.setEn(name.getValue()); + } else if (Language.DE.getValue().equalsIgnoreCase(name.getLocale())) { + value.setDe(name.getValue()); + } + } + } + + return value; + } + private static String extractParentDirectory(GHContent ghContent) { var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); return path.replace(ghContent.getName(), EMPTY); diff --git a/src/main/java/com/axonivy/market/github/model/Meta.java b/src/main/java/com/axonivy/market/github/model/Meta.java index 4ef988820..ee80857de 100644 --- a/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/src/main/java/com/axonivy/market/github/model/Meta.java @@ -2,6 +2,7 @@ import java.util.List; +import com.axonivy.market.model.DisplayValue; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -19,8 +20,8 @@ public class Meta { @JsonProperty("$schema") private String schema; private String id; - private String name; - private String description; + private List names; + private List descriptions; private String type; private String platformReview; private String sourceUrl; diff --git a/src/main/java/com/axonivy/market/github/service/GitHubService.java b/src/main/java/com/axonivy/market/github/service/GitHubService.java index 2f4fb7e16..7dd5009db 100644 --- a/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -16,7 +16,7 @@ public interface GitHubService { public GHRepository getRepository(String repositoryPath) throws IOException; - public List getDirectoryContent(GHRepository ghRepository, String path) throws IOException; + public List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; - public GHContent getGHContent(GHRepository ghRepository, String path) throws IOException; + public GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; } diff --git a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index 0d5202c18..6c96bee26 100644 --- a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -29,7 +29,6 @@ @Log4j2 @Service public class GHAxonIvyMarketRepoServiceImpl implements GHAxonIvyMarketRepoService { - private static final String DEFAULT_BRANCH = "master"; private static final LocalDateTime INITIAL_COMMIT_DATE = LocalDateTime.of(2020, 10, 30, 0, 0); private GHOrganization organization; private GHRepository repository; @@ -45,7 +44,7 @@ public Map> fetchAllMarketItems() { Map> ghContentMap = new HashMap<>(); try { List directoryContent = gitHubService.getDirectoryContent(getRepository(), - GitHubConstants.AXONIVY_MARKETPLACE_PATH); + GitHubConstants.AXONIVY_MARKETPLACE_PATH, GitHubConstants.DEFAULT_BRANCH); for (var content : directoryContent) { extractFileInDirectoryContent(content, ghContentMap); } @@ -85,7 +84,7 @@ public GHCommit getLastCommit(long lastCommitTime) { } private GHCommitQueryBuilder createQueryCommitsBuilder(long lastCommitTime) { - return getRepository().queryCommits().since(lastCommitTime).from(DEFAULT_BRANCH); + return getRepository().queryCommits().since(lastCommitTime).from(GitHubConstants.DEFAULT_BRANCH); } @Override diff --git a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index b1a069ed5..62bbd9181 100644 --- a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -34,9 +34,9 @@ public GHOrganization getOrganization(String orgName) throws IOException { } @Override - public List getDirectoryContent(GHRepository ghRepository, String path) throws IOException { + public List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException { Assert.notNull(ghRepository, "Repository must not be null"); - return ghRepository.getDirectoryContent(path); + return ghRepository.getDirectoryContent(path, ref); } @Override @@ -45,8 +45,8 @@ public GHRepository getRepository(String repositoryPath) throws IOException { } @Override - public GHContent getGHContent(GHRepository ghRepository, String path) throws IOException { + public GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException { Assert.notNull(ghRepository, "Repository must not be null"); - return ghRepository.getFileContent(path); + return ghRepository.getFileContent(path, ref); } } \ No newline at end of file diff --git a/src/main/java/com/axonivy/market/model/DisplayValue.java b/src/main/java/com/axonivy/market/model/DisplayValue.java new file mode 100644 index 000000000..cd0bf4abf --- /dev/null +++ b/src/main/java/com/axonivy/market/model/DisplayValue.java @@ -0,0 +1,44 @@ +package com.axonivy.market.model; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DisplayValue { + + private String locale; + private String value; + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DisplayValue)) { + return false; + } + DisplayValue other = (DisplayValue) obj; + EqualsBuilder builder = new EqualsBuilder(); + builder.append(value, other.getValue()); + builder.append(locale, other.locale); + return builder.isEquals(); + } + + @Override + public int hashCode() { + HashCodeBuilder builder = new HashCodeBuilder(); + builder.append(getValue()); + builder.append(getLocale()); + return builder.hashCode(); + } +} diff --git a/src/main/java/com/axonivy/market/model/MultilingualismValue.java b/src/main/java/com/axonivy/market/model/MultilingualismValue.java new file mode 100644 index 000000000..58431cf8e --- /dev/null +++ b/src/main/java/com/axonivy/market/model/MultilingualismValue.java @@ -0,0 +1,17 @@ +package com.axonivy.market.model; + +import java.io.Serializable; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class MultilingualismValue implements Serializable { + private static final long serialVersionUID = -4193508237020296419L; + + private String en; + private String de; +} diff --git a/src/main/java/com/axonivy/market/model/ProductModel.java b/src/main/java/com/axonivy/market/model/ProductModel.java index f11a62244..7ba586438 100644 --- a/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/src/main/java/com/axonivy/market/model/ProductModel.java @@ -21,8 +21,8 @@ @JsonInclude(Include.NON_NULL) public class ProductModel extends RepresentationModel { private String id; - private String name; - private String shortDescription; + private MultilingualismValue names; + private MultilingualismValue shortDescriptions; private String logoUrl; private String type; private List tags; diff --git a/src/main/java/com/axonivy/market/repository/ProductRepository.java b/src/main/java/com/axonivy/market/repository/ProductRepository.java index 1b18bad04..25ff5e794 100644 --- a/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -22,9 +22,9 @@ public interface ProductRepository extends MongoRepository { @Query("{'marketDirectory': {$regex : ?0, $options: 'i'}}") Product findByMarketDirectoryRegex(String search); - @Query("{ $and: [ { $or: [ { 'name': { $regex: ?0, $options: 'i' } }, { 'shortDescription': { $regex: ?0, $options: 'i' } } ] }, { 'type': ?1 } ] }") - Page searchByKeywordAndType(String keyword, String type, Pageable unifiedPageabe); + @Query("{ $and: [ { $or: [ { 'names.?': { $regex: ?0, $options: 'i' } }, { 'shortDescriptions.?': { $regex: ?0, $options: 'i' } } ] }, { 'type': ?1 } ] }") + Page searchByKeywordAndType(String keyword, String type, String language, Pageable unifiedPageabe); - @Query("{ $or: [ { 'name': { $regex: ?0, $options: 'i' } }, { 'shortDescription': { $regex: ?0, $options: 'i' } } ] }") - Page searchByNameOrShortDescriptionRegex(String keyword, Pageable unifiedPageabe); + @Query("{ $or: [ { 'names.?1': { $regex: ?0, $options: 'i' } }, { 'shortDescriptions.?1': { $regex: ?0, $options: 'i' } } ] }") + Page searchByNameOrShortDescriptionRegex(String keyword, String language, Pageable unifiedPageabe); } diff --git a/src/main/java/com/axonivy/market/service/ProductService.java b/src/main/java/com/axonivy/market/service/ProductService.java index 0a3f5529f..5e35368de 100644 --- a/src/main/java/com/axonivy/market/service/ProductService.java +++ b/src/main/java/com/axonivy/market/service/ProductService.java @@ -6,7 +6,7 @@ import com.axonivy.market.entity.Product; public interface ProductService { - Page findProducts(String type, String keyword, Pageable pageable); + Page findProducts(String type, String keyword, String language, Pageable pageable); boolean syncLatestDataFromMarketRepo(); } diff --git a/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 44de63b60..70f7a3a16 100644 --- a/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -63,23 +63,23 @@ public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRe } @Override - public Page findProducts(String type, String keyword, Pageable pageable) { + public Page findProducts(String type, String keyword, String language, Pageable pageable) { final var typeOption = TypeOption.of(type); - final var searchPageable = refinePagination(pageable); + final var searchPageable = refinePagination(language, pageable); Page result = Page.empty(); switch (typeOption) { case ALL: if (StringUtils.isBlank(keyword)) { result = productRepository.findAll(searchPageable); } else { - result = productRepository.searchByNameOrShortDescriptionRegex(keyword, searchPageable); + result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); } break; case CONNECTORS, UTILITIES, SOLUTIONS: if (StringUtils.isBlank(keyword)) { result = productRepository.findByType(typeOption.getCode(), searchPageable); } else { - result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), searchPageable); + result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); } break; default: @@ -136,7 +136,8 @@ private void updateLatestChangeToProductsFromGithubRepo() { Product product = new Product(); GHContent fileContent; try { - fileContent = gitHubService.getGHContent(axonIvyMarketRepoService.getRepository(), file.getFileName()); + fileContent = gitHubService.getGHContent(axonIvyMarketRepoService.getRepository(), file.getFileName(), + GitHubConstants.DEFAULT_BRANCH); } catch (IOException e) { log.error("Get GHContent failed: ", e); continue; @@ -187,13 +188,13 @@ private void modifyProductByMetaContent(GitHubFile file, Product product) { } } - private Pageable refinePagination(Pageable pageable) { + private Pageable refinePagination(String language, Pageable pageable) { PageRequest pageRequest = (PageRequest) pageable; - if (pageable != null && pageable.getSort() != null) { + if (pageable != null) { List orders = new ArrayList<>(); for (var sort : pageable.getSort()) { final var sortOption = SortOption.of(sort.getProperty()); - Order order = new Order(sort.getDirection(), sortOption.getCode()); + Order order = new Order(sort.getDirection(), sortOption.getCode(language)); orders.add(order); } pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders)); diff --git a/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index a2c6f3ec7..00417f662 100644 --- a/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -28,12 +28,15 @@ import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.enums.SortOption; import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.service.ProductService; @ExtendWith(MockitoExtension.class) class ProductControllerTest { private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; + private static final String PRODUCT_NAME_DE_SAMPLE = "Amazon Comprehend DE"; private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; + private static final String PRODUCT_DESC_DE_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data. DE"; @Mock private ProductService service; @@ -59,9 +62,9 @@ void setup() { void testFindProductsAsEmpty() { PageRequest pageable = PageRequest.of(0, 20); Page mockProducts = new PageImpl(List.of(), pageable, 0); - when(service.findProducts(any(), any(), any())).thenReturn(mockProducts); + when(service.findProducts(any(), any(), any(), any())).thenReturn(mockProducts); when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); - var result = productController.findProducts(TypeOption.ALL.getOption(), null, pageable); + var result = productController.findProducts(TypeOption.ALL.getOption(), null, "en", pageable); assertEquals(HttpStatus.OK, result.getStatusCode()); assertTrue(result.hasBody()); assertEquals(0, result.getBody().getContent().size()); @@ -73,16 +76,17 @@ void testFindProducts() { Product mockProduct = createProductMock(); Page mockProducts = new PageImpl(List.of(mockProduct), pageable, 1); - when(service.findProducts(any(), any(), any())).thenReturn(mockProducts); + when(service.findProducts(any(), any(), any(), any())).thenReturn(mockProducts); assembler = new ProductModelAssembler(); var mockProductModel = assembler.toModel(mockProduct); var mockPagedModel = PagedModel.of(List.of(mockProductModel), new PageMetadata(1, 0, 1)); when(pagedResourcesAssembler.toModel(any(), any(ProductModelAssembler.class))).thenReturn(mockPagedModel); - var result = productController.findProducts(TypeOption.ALL.getOption(), null, pageable); + var result = productController.findProducts(TypeOption.ALL.getOption(), "", "en", pageable); assertEquals(HttpStatus.OK, result.getStatusCode()); assertTrue(result.hasBody()); assertEquals(1, result.getBody().getContent().size()); - assertEquals(PRODUCT_NAME_SAMPLE, result.getBody().getContent().iterator().next().getName()); + assertEquals(PRODUCT_NAME_SAMPLE, result.getBody().getContent().iterator().next().getNames().getEn()); + assertEquals(PRODUCT_NAME_DE_SAMPLE, result.getBody().getContent().iterator().next().getNames().getDe()); } @Test @@ -96,8 +100,14 @@ void testSyncProducts() { private Product createProductMock() { Product mockProduct = new Product(); mockProduct.setId("amazon-comprehend"); - mockProduct.setName(PRODUCT_NAME_SAMPLE); - mockProduct.setShortDescription(PRODUCT_DESC_SAMPLE); + MultilingualismValue name = new MultilingualismValue(); + name.setEn(PRODUCT_NAME_SAMPLE); + name.setDe(PRODUCT_NAME_DE_SAMPLE); + mockProduct.setNames(name); + MultilingualismValue shortDescription = new MultilingualismValue(); + shortDescription.setEn(PRODUCT_DESC_SAMPLE); + shortDescription.setDe(PRODUCT_DESC_DE_SAMPLE); + mockProduct.setShortDescriptions(shortDescription); mockProduct.setType("connector"); mockProduct.setTags(List.of("AI")); return mockProduct; diff --git a/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index 96d27d5bf..0221c4ce6 100644 --- a/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -22,54 +22,56 @@ @ExtendWith(MockitoExtension.class) class ProductFactoryTest { - private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; + private static final String DUMMY_LOGO_URL = + "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; - @Test - void testMappingByGHContent() throws IOException { - Product product = new Product(); - GHContent mockContent = mock(GHContent.class); - var result = ProductFactory.mappingByGHContent(product, null); - assertEquals(product, result); - when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); - InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); - when(mockContent.read()).thenReturn(inputStream); - result = ProductFactory.mappingByGHContent(product, mockContent); - assertNotEquals(null, result); - assertEquals("Amazon Comprehend", result.getName()); - } + @Test + void testMappingByGHContent() throws IOException { + Product product = new Product(); + GHContent mockContent = mock(GHContent.class); + var result = ProductFactory.mappingByGHContent(product, null); + assertEquals(product, result); + when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); + InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); + when(mockContent.read()).thenReturn(inputStream); + result = ProductFactory.mappingByGHContent(product, mockContent); + assertNotEquals(null, result); + assertEquals("Amazon Comprehend", result.getNames().getEn()); + assertEquals("Amazon Comprehend DE", result.getNames().getDe()); + } - @Test - void testMappingLogo() throws IOException { - Product product = new Product(); - GHContent content = mock(GHContent.class); - when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); - var result = ProductFactory.mappingByGHContent(product, content); - assertNotEquals(null, result); + @Test + void testMappingLogo() throws IOException { + Product product = new Product(); + GHContent content = mock(GHContent.class); + when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + var result = ProductFactory.mappingByGHContent(product, content); + assertNotEquals(null, result); - when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); - when(content.getDownloadUrl()).thenReturn(DUMMY_LOGO_URL); - result = ProductFactory.mappingByGHContent(product, content); - assertNotEquals(null, result); - } + when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + when(content.getDownloadUrl()).thenReturn(DUMMY_LOGO_URL); + result = ProductFactory.mappingByGHContent(product, content); + assertNotEquals(null, result); + } - @Test - void testExtractSourceUrl() throws IOException { - Product product = new Product(); - Meta meta = new Meta(); - ProductFactory.extractSourceUrl(product, meta); - Assertions.assertNull(product.getRepositoryName()); - Assertions.assertNull(product.getSourceUrl()); + @Test + void testExtractSourceUrl() throws IOException { + Product product = new Product(); + Meta meta = new Meta(); + ProductFactory.extractSourceUrl(product, meta); + Assertions.assertNull(product.getRepositoryName()); + Assertions.assertNull(product.getSourceUrl()); - String sourceUrl = "https://github.com/axonivy-market/alfresco-connector"; - meta.setSourceUrl(sourceUrl); - ProductFactory.extractSourceUrl(product, meta); - Assertions.assertEquals("axonivy-market/alfresco-connector", product.getRepositoryName()); - Assertions.assertEquals(sourceUrl, product.getSourceUrl()); + String sourceUrl = "https://github.com/axonivy-market/alfresco-connector"; + meta.setSourceUrl(sourceUrl); + ProductFactory.extractSourceUrl(product, meta); + Assertions.assertEquals("axonivy-market/alfresco-connector", product.getRepositoryName()); + Assertions.assertEquals(sourceUrl, product.getSourceUrl()); - sourceUrl = "portal"; - meta.setSourceUrl(sourceUrl); - ProductFactory.extractSourceUrl(product, meta); - Assertions.assertEquals(sourceUrl, product.getRepositoryName()); - Assertions.assertEquals(sourceUrl, product.getSourceUrl()); - } + sourceUrl = "portal"; + meta.setSourceUrl(sourceUrl); + ProductFactory.extractSourceUrl(product, meta); + Assertions.assertEquals(sourceUrl, product.getRepositoryName()); + Assertions.assertEquals(sourceUrl, product.getSourceUrl()); + } } diff --git a/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java index d8b06b074..fa74bc54a 100644 --- a/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -73,7 +73,7 @@ void testFetchAllMarketItems() throws IOException { mockGhContents.add(mockGHContent); when(mockGHFileContent.isFile()).thenReturn(true); when(pagedGHContent.toList()).thenReturn(List.of(mockGHFileContent)); - when(gitHubService.getDirectoryContent(any(), any())).thenReturn(mockGhContents); + when(gitHubService.getDirectoryContent(any(), any(), any())).thenReturn(mockGhContents); ghContentMap = axonIvyMarketRepoServiceImpl.fetchAllMarketItems(); assertEquals(1, ghContentMap.values().size()); diff --git a/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index ea3bd0fe7..e26226c6b 100644 --- a/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -41,14 +41,14 @@ void testGetGithubContent() throws IOException { var mockGHContent = mock(GHContent.class); final String dummryURL = DUMMY_API_URL.concat("/dummry-content"); when(mockGHContent.getUrl()).thenReturn(dummryURL); - when(ghRepository.getFileContent(any())).thenReturn(mockGHContent); - var result = gitHubService.getGHContent(ghRepository, ""); + when(ghRepository.getFileContent(any(), any())).thenReturn(mockGHContent); + var result = gitHubService.getGHContent(ghRepository, "", ""); assertEquals(dummryURL, result.getUrl()); } @Test void testGetDirectoryContent() throws IOException { - var result = gitHubService.getDirectoryContent(ghRepository, ""); + var result = gitHubService.getDirectoryContent(ghRepository, "", ""); assertEquals(0, result.size()); } diff --git a/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 5671a21c5..bcebfd4e8 100644 --- a/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -46,6 +46,7 @@ import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.ProductServiceImpl; @@ -60,6 +61,7 @@ class ProductServiceImplTest { Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; private String keyword; + private String langague; private Page mockResultReturn; @Mock @@ -84,21 +86,22 @@ public void setup() { @Test void testFindProducts() { + langague = "en"; // Start testing by All when(productRepository.findAll(any(Pageable.class))).thenReturn(mockResultReturn); // Executes - var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, PAGEABLE); + var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, langague, PAGEABLE); assertEquals(mockResultReturn, result); // Start testing by Connector when(productRepository.findByType(any(), any(Pageable.class))).thenReturn(mockResultReturn); // Executes - result = productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, PAGEABLE); + result = productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, langague, PAGEABLE); assertEquals(mockResultReturn, result); // Start testing by Other // Executes - result = productService.findProducts(TypeOption.DEMOS.getOption(), keyword, PAGEABLE); + result = productService.findProducts(TypeOption.DEMOS.getOption(), keyword, langague, PAGEABLE); assertEquals(0, result.getSize()); } @@ -116,7 +119,7 @@ void testSyncProductsAsUpdateMetaJSONFromGitHub() throws IOException { mockGithubFile.setStatus(FileStatus.ADDED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGithubFile)); var mockGHContent = mockGHContentAsMetaJSON(); - when(gitHubService.getGHContent(any(), anyString())).thenReturn(mockGHContent); + when(gitHubService.getGHContent(any(), anyString(), anyString())).thenReturn(mockGHContent); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -147,7 +150,7 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { mockGitHubFile.setStatus(FileStatus.ADDED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); var mockGHContent = mockGHContentAsMetaJSON(); - when(gitHubService.getGHContent(any(), anyString())).thenReturn(mockGHContent); + when(gitHubService.getGHContent(any(), anyString(), anyString())).thenReturn(mockGHContent); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -157,7 +160,7 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { when(mockCommit.getSHA1()).thenReturn(UUID.randomUUID().toString()); mockGitHubFile.setStatus(FileStatus.REMOVED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - when(gitHubService.getGHContent(any(), anyString())).thenReturn(mockGHContent); + when(gitHubService.getGHContent(any(), anyString(), anyString())).thenReturn(mockGHContent); when(productRepository.findByLogoUrl(any())).thenReturn(new Product()); // Executes @@ -167,30 +170,31 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { @Test void testFindAllProductsWithKeyword() throws IOException { + langague = "en"; when(productRepository.findAll(any(Pageable.class))).thenReturn(mockResultReturn); // Executes - var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, PAGEABLE); + var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, langague, PAGEABLE); assertEquals(mockResultReturn, result); verify(productRepository).findAll(any(Pageable.class)); // Test has keyword - when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(Pageable.class))) + when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(), any(Pageable.class))) .thenReturn(new PageImpl<>(mockResultReturn.stream() - .filter(product -> product.getName().equals(SAMPLE_PRODUCT_NAME)).collect(Collectors.toList()))); + .filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME)).collect(Collectors.toList()))); // Executes - result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, PAGEABLE); + result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); verify(productRepository).findAll(any(Pageable.class)); assertTrue(result.hasContent()); - assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getName()); + assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); // Test has keyword and type is connector - when(productRepository.searchByKeywordAndType(any(), any(), any(Pageable.class))).thenReturn( - new PageImpl<>(mockResultReturn.stream().filter(product -> product.getName().equals(SAMPLE_PRODUCT_NAME) + when(productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))).thenReturn( + new PageImpl<>(mockResultReturn.stream().filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) && product.getType().equals(TypeOption.CONNECTORS.getCode())).collect(Collectors.toList()))); // Executes - result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, PAGEABLE); + result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); assertTrue(result.hasContent()); - assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getName()); + assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); } @Test @@ -230,24 +234,29 @@ void testSearchProducts() { var simplePageable = PageRequest.of(0, 20); String type = TypeOption.ALL.getOption(); keyword = "on"; - when(productRepository.searchByNameOrShortDescriptionRegex(keyword, simplePageable)).thenReturn(mockResultReturn); + langague = "en"; + when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)).thenReturn(mockResultReturn); - var result = productService.findProducts(type, keyword, simplePageable); + var result = productService.findProducts(type, keyword, langague, simplePageable); assertEquals(result, mockResultReturn); - verify(productRepository).searchByNameOrShortDescriptionRegex(keyword, simplePageable); + verify(productRepository).searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable); } private Page createPageProductsMock() { var mockProducts = new ArrayList(); + MultilingualismValue name = new MultilingualismValue(); Product mockProduct = new Product(); mockProduct.setId(SAMPLE_PRODUCT_ID); - mockProduct.setName(SAMPLE_PRODUCT_NAME); + name.setEn(SAMPLE_PRODUCT_NAME); + mockProduct.setNames(name); mockProduct.setType("connector"); mockProducts.add(mockProduct); mockProduct = new Product(); mockProduct.setId("tel-search-ch-connector"); - mockProduct.setName("Swiss phone directory"); + name = new MultilingualismValue(); + name.setEn("Swiss phone directory"); + mockProduct.setNames(name); mockProduct.setType("util"); mockProducts.add(mockProduct); return new PageImpl<>(mockProducts); diff --git a/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java index 932b7a864..f9f75b439 100644 --- a/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java +++ b/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java @@ -1,5 +1,31 @@ package com.axonivy.market.service; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Fail; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.constants.NonStandardProductPackageConstants; import com.axonivy.market.entity.MavenArtifactModel; @@ -13,26 +39,7 @@ import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.VersionServiceImpl; import com.axonivy.market.utils.XmlReaderUtils; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.api.Fail; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHContent; -import org.mockito.*; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -import java.io.IOException; -import java.util.*; -@Log4j2 @ExtendWith(MockitoExtension.class) class VersionServiceImplTest { private String repoName; diff --git a/src/test/resources/meta.json b/src/test/resources/meta.json index 90fb8f309..d46c28424 100644 --- a/src/test/resources/meta.json +++ b/src/test/resources/meta.json @@ -1,8 +1,26 @@ { "$schema": "https://json-schema.axonivy.com/market/10.0.0/meta.json", "id": "amazon-comprehend", - "name": "Amazon Comprehend", - "description": "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", + "names": [ + { + "locale":"en", + "value": "Amazon Comprehend" + }, + { + "locale":"de", + "value": "Amazon Comprehend DE" + } + ], + "descriptions": [ + { + "locale":"en", + "value": "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data." + }, + { + "locale":"de", + "value": "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data. DE" + } + ], "type": "connector", "platformReview": "4.5", "sourceUrl": "https://github.com/axonivy-market/amazon-comprehend-connector", From 79ce2ac42c967e6b4591fef049afcdf36dad4e07 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:09:46 +0700 Subject: [PATCH 13/62] Feature/marp 566 retructure prepare for docker (#25) Co-authored-by: Hoan Nguyen --- .github/workflows/service-ci-build.yml | 50 + .../{dev-build.yml => service-dev-build.yml} | 0 .github/workflows/ui-ci-build.yml | 57 + .github/workflows/ui-dev-build.yml | 28 + CODE_OF_CONDUCT.md | 24 + marketplace-build/Dockerfile | 1 + .../Marketplace Tomcat v10.1 Server.launch | 22 + marketplace-build/docker-compose.yml | 1 + marketplace-service/.gitignore | 36 + marketplace-service/README.md | 34 + .../lombok.config | 0 pom.xml => marketplace-service/pom.xml | 0 .../sonar-project.properties | 0 .../market/MarketplaceServiceApplication.java | 0 .../axonivy/market/ServletInitializer.java | 0 .../assembler/ProductModelAssembler.java | 0 .../ArchivedArtifactsComparator.java | 0 .../comparator/LatestVersionComparator.java | 0 .../config/MarketApiDocumentConfig.java | 0 .../config/MarketHeaderInterceptor.java | 0 .../axonivy/market/config/MongoConfig.java | 0 .../com/axonivy/market/config/WebConfig.java | 0 .../market/constants/CommonConstants.java | 0 .../market/constants/EntityConstants.java | 0 .../constants/ErrorMessageConstants.java | 0 .../market/constants/GitHubConstants.java | 0 .../market/constants/MavenConstants.java | 0 .../NonStandardProductPackageConstants.java | 0 .../constants/ProductJsonConstants.java | 0 .../constants/RequestMappingConstants.java | 0 .../market/controller/AppController.java | 0 .../market/controller/ProductController.java | 0 .../controller/ProductDetailsController.java | 0 .../market/controller/UserController.java | 0 .../axonivy/market/entity/GitHubRepoMeta.java | 0 .../market/entity/MavenArtifactModel.java | 0 .../market/entity/MavenArtifactVersion.java | 0 .../com/axonivy/market/entity/Product.java | 0 .../java/com/axonivy/market/entity/User.java | 0 .../com/axonivy/market/enums/ErrorCode.java | 0 .../com/axonivy/market/enums/FileStatus.java | 0 .../com/axonivy/market/enums/FileType.java | 0 .../com/axonivy/market/enums/Language.java | 0 .../com/axonivy/market/enums/SortOption.java | 0 .../com/axonivy/market/enums/TypeOption.java | 0 .../market/exceptions/ExceptionHandlers.java | 0 .../model/InvalidParamException.java | 0 .../model/MissingHeaderException.java | 0 .../exceptions/model/NotFoundException.java | 0 .../market/factory/ProductFactory.java | 0 .../market/github/model/ArchivedArtifact.java | 0 .../market/github/model/GitHubFile.java | 0 .../market/github/model/MavenArtifact.java | 0 .../com/axonivy/market/github/model/Meta.java | 0 .../service/GHAxonIvyMarketRepoService.java | 0 .../service/GHAxonIvyProductRepoService.java | 0 .../market/github/service/GitHubService.java | 0 .../impl/GHAxonIvyMarketRepoServiceImpl.java | 0 .../impl/GHAxonIvyProductRepoServiceImpl.java | 0 .../service/impl/GitHubServiceImpl.java | 0 .../market/github/util/GitHubUtils.java | 0 .../axonivy/market/model/DisplayValue.java | 0 .../model/MavenArtifactVersionModel.java | 0 .../com/axonivy/market/model/Message.java | 0 .../market/model/MultilingualismValue.java | 0 .../axonivy/market/model/ProductModel.java | 0 .../repository/GitHubRepoMetaRepository.java | 0 .../MavenArtifactVersionRepository.java | 0 .../market/repository/ProductRepository.java | 0 .../market/repository/UserRepository.java | 0 .../market/schedulingtask/ScheduledTasks.java | 0 .../market/service/ProductService.java | 0 .../axonivy/market/service/UserService.java | 0 .../market/service/VersionService.java | 0 .../service/impl/ProductServiceImpl.java | 0 .../market/service/impl/UserServiceImpl.java | 0 .../service/impl/VersionServiceImpl.java | 0 .../axonivy/market/utils/XmlReaderUtils.java | 0 .../main/resources/application.properties | 0 .../market/controller/AppControllerTest.java | 0 .../controller/ProductControllerTest.java | 0 .../ProductDetailsControllerTest.java | 0 .../market/controller/UserControllerTest.java | 0 .../market/factory/ProductFactoryTest.java | 0 .../GHAxonIvyProductRepoServiceImplTest.java | 0 .../market/handler/ExceptionHandlersTest.java | 0 .../GHAxonIvyMarketRepoServiceImplTest.java | 0 .../market/service/GitHubServiceImplTest.java | 0 .../service/ProductServiceImplTest.java | 0 .../market/service/SchedulingTasksTest.java | 0 .../market/service/UserServiceImplTest.java | 0 .../service/VersionServiceImplTest.java | 0 .../market/utils/XmlReaderUtilsTest.java | 0 .../src}/test/resources/meta.json | 0 marketplace-ui/.editorconfig | 16 + marketplace-ui/.gitignore | 48 + marketplace-ui/.prettierrc | 15 + marketplace-ui/.vscode/extensions.json | 4 + marketplace-ui/.vscode/launch.json | 20 + marketplace-ui/.vscode/tasks.json | 42 + marketplace-ui/README.md | 27 + marketplace-ui/angular.json | 124 + marketplace-ui/karma.conf.js | 58 + marketplace-ui/package-lock.json | 14063 ++++++++++++++++ marketplace-ui/package.json | 55 + marketplace-ui/sonar-project.properties | 5 + marketplace-ui/src/app/app.component.html | 21 + marketplace-ui/src/app/app.component.scss | 44 + marketplace-ui/src/app/app.component.spec.ts | 30 + marketplace-ui/src/app/app.component.ts | 16 + marketplace-ui/src/app/app.config.ts | 33 + marketplace-ui/src/app/app.routes.ts | 14 + .../src/app/core/configs/translate.config.ts | 21 + .../app/core/interceptors/api.interceptor.ts | 46 + .../language/language.service.spec.ts | 30 + .../services/language/language.service.ts | 28 + .../services/loading/loading.service.spec.ts | 26 + .../core/services/loading/loading.service.ts | 17 + .../core/services/theme/theme.service.spec.ts | 42 + .../app/core/services/theme/theme.service.ts | 46 + .../src/app/modules/home/home.component.html | 3 + .../src/app/modules/home/home.component.scss | 7 + .../app/modules/home/home.component.spec.ts | 33 + .../src/app/modules/home/home.component.ts | 11 + .../src/app/modules/home/home.routes.ts | 9 + .../product-card/product-card.component.html | 28 + .../product-card/product-card.component.scss | 26 + .../product-card.component.spec.ts | 54 + .../product-card/product-card.component.ts | 22 + ...oduct-detail-version-action.component.html | 85 + ...oduct-detail-version-action.component.scss | 178 + ...ct-detail-version-action.component.spec.ts | 174 + ...product-detail-version-action.component.ts | 133 + .../product-detail.component.html | 18 + .../product-detail.component.scss | 0 .../product-detail.component.spec.ts | 48 + .../product-detail.component.ts | 33 + .../product-filter.component.html | 79 + .../product-filter.component.scss | 64 + .../product-filter.component.spec.ts | 76 + .../product-filter.component.ts | 47 + .../modules/product/product.component.html | 45 + .../modules/product/product.component.scss | 3 + .../modules/product/product.component.spec.ts | 159 + .../app/modules/product/product.component.ts | 155 + .../src/app/modules/product/product.routes.ts | 11 + .../modules/product/product.service.spec.ts | 183 + .../app/modules/product/product.service.ts | 62 + .../components/footer/footer.component.html | 89 + .../components/footer/footer.component.scss | 38 + .../footer/footer.component.spec.ts | 73 + .../components/footer/footer.component.ts | 24 + .../components/header/header.component.html | 38 + .../components/header/header.component.scss | 59 + .../header/header.component.spec.ts | 105 + .../components/header/header.component.ts | 47 + .../language-selection.component.html | 14 + .../language-selection.component.scss | 18 + .../language-selection.component.spec.ts | 30 + .../language-selection.component.ts | 27 + .../navigation/navigation.component.html | 36 + .../navigation/navigation.component.scss | 61 + .../navigation/navigation.component.spec.ts | 48 + .../header/navigation/navigation.component.ts | 18 + .../search-bar/search-bar.component.html | 81 + .../search-bar/search-bar.component.scss | 33 + .../search-bar/search-bar.component.spec.ts | 74 + .../header/search-bar/search-bar.component.ts | 50 + .../theme-selection.component.html | 9 + .../theme-selection.component.scss | 10 + .../theme-selection.component.spec.ts | 37 + .../theme-selection.component.ts | 13 + .../app/shared/constants/common.constant.ts | 110 + .../src/app/shared/enums/language.enum.ts | 4 + .../src/app/shared/enums/request-param.ts | 6 + .../src/app/shared/enums/sort-option.enum.ts | 5 + .../src/app/shared/enums/theme.enum.ts | 4 + .../src/app/shared/enums/type-option.enum.ts | 7 + .../src/app/shared/mocks/mock-data.ts | 209 + .../src/app/shared/mocks/mock-services.ts | 24 + .../src/app/shared/models/apis/link.model.ts | 14 + .../src/app/shared/models/apis/page.model.ts | 6 + .../models/apis/product-response.model.ts | 11 + .../src/app/shared/models/criteria.model.ts | 10 + .../app/shared/models/display-value.model.ts | 5 + .../app/shared/models/maven-artifact.model.ts | 16 + .../src/app/shared/models/nav-item.model.ts | 4 + .../src/app/shared/models/product.model.ts | 33 + .../shared/models/vesion-artifact.model.ts | 10 + .../src/app/shared/pipes/logo.pipe.ts | 16 + .../app/shared/pipes/multilingualism.pipe.ts | 21 + marketplace-ui/src/assets/.gitkeep | 0 marketplace-ui/src/assets/fonts/inter.ttf | Bin 0 -> 804612 bytes marketplace-ui/src/assets/i18n/de.yaml | 55 + marketplace-ui/src/assets/i18n/en.yaml | 59 + .../assets/images/misc/axonivy-logo-black.svg | 1 + .../assets/images/misc/axonivy-logo-round.png | Bin 0 -> 2418 bytes .../src/assets/images/misc/axonivy-logo.svg | 1 + .../src/assets/scss/custom-style.scss | 243 + .../environments/environment.development.ts | 4 + .../src/environments/environment.ts | 4 + marketplace-ui/src/favicon.ico | Bin 0 -> 701 bytes marketplace-ui/src/index.html | 13 + marketplace-ui/src/main.ts | 9 + marketplace-ui/src/styles.scss | 9 + marketplace-ui/tsconfig.app.json | 17 + marketplace-ui/tsconfig.json | 33 + marketplace-ui/tsconfig.spec.json | 15 + src/main/resources/github.token | 1 - 209 files changed, 18900 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/service-ci-build.yml rename .github/workflows/{dev-build.yml => service-dev-build.yml} (100%) create mode 100644 .github/workflows/ui-ci-build.yml create mode 100644 .github/workflows/ui-dev-build.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 marketplace-build/Dockerfile create mode 100644 marketplace-build/Marketplace Tomcat v10.1 Server.launch create mode 100644 marketplace-build/docker-compose.yml create mode 100644 marketplace-service/.gitignore create mode 100644 marketplace-service/README.md rename lombok.config => marketplace-service/lombok.config (100%) rename pom.xml => marketplace-service/pom.xml (100%) rename sonar-project.properties => marketplace-service/sonar-project.properties (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/MarketplaceServiceApplication.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/ServletInitializer.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/assembler/ProductModelAssembler.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/comparator/LatestVersionComparator.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/config/MongoConfig.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/config/WebConfig.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/CommonConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/EntityConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/ErrorMessageConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/GitHubConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/MavenConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/ProductJsonConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/constants/RequestMappingConstants.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/controller/AppController.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/controller/ProductController.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/controller/ProductDetailsController.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/controller/UserController.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/entity/GitHubRepoMeta.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/entity/MavenArtifactModel.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/entity/MavenArtifactVersion.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/entity/Product.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/entity/User.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/enums/ErrorCode.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/enums/FileStatus.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/enums/FileType.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/enums/Language.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/enums/SortOption.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/enums/TypeOption.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/exceptions/model/NotFoundException.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/factory/ProductFactory.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/model/ArchivedArtifact.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/model/GitHubFile.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/model/MavenArtifact.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/model/Meta.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/service/GitHubService.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/github/util/GitHubUtils.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/model/DisplayValue.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/model/Message.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/model/MultilingualismValue.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/model/ProductModel.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/repository/ProductRepository.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/repository/UserRepository.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/service/ProductService.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/service/UserService.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/service/VersionService.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/service/impl/UserServiceImpl.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java (100%) rename {src => marketplace-service/src}/main/java/com/axonivy/market/utils/XmlReaderUtils.java (100%) rename {src => marketplace-service/src}/main/resources/application.properties (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/controller/AppControllerTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/controller/ProductControllerTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/controller/UserControllerTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/factory/ProductFactoryTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/service/GitHubServiceImplTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/service/ProductServiceImplTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/service/SchedulingTasksTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/service/UserServiceImplTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/service/VersionServiceImplTest.java (100%) rename {src => marketplace-service/src}/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java (100%) rename {src => marketplace-service/src}/test/resources/meta.json (100%) create mode 100644 marketplace-ui/.editorconfig create mode 100644 marketplace-ui/.gitignore create mode 100644 marketplace-ui/.prettierrc create mode 100644 marketplace-ui/.vscode/extensions.json create mode 100644 marketplace-ui/.vscode/launch.json create mode 100644 marketplace-ui/.vscode/tasks.json create mode 100644 marketplace-ui/README.md create mode 100644 marketplace-ui/angular.json create mode 100644 marketplace-ui/karma.conf.js create mode 100644 marketplace-ui/package-lock.json create mode 100644 marketplace-ui/package.json create mode 100644 marketplace-ui/sonar-project.properties create mode 100644 marketplace-ui/src/app/app.component.html create mode 100644 marketplace-ui/src/app/app.component.scss create mode 100644 marketplace-ui/src/app/app.component.spec.ts create mode 100644 marketplace-ui/src/app/app.component.ts create mode 100644 marketplace-ui/src/app/app.config.ts create mode 100644 marketplace-ui/src/app/app.routes.ts create mode 100644 marketplace-ui/src/app/core/configs/translate.config.ts create mode 100644 marketplace-ui/src/app/core/interceptors/api.interceptor.ts create mode 100644 marketplace-ui/src/app/core/services/language/language.service.spec.ts create mode 100644 marketplace-ui/src/app/core/services/language/language.service.ts create mode 100644 marketplace-ui/src/app/core/services/loading/loading.service.spec.ts create mode 100644 marketplace-ui/src/app/core/services/loading/loading.service.ts create mode 100644 marketplace-ui/src/app/core/services/theme/theme.service.spec.ts create mode 100644 marketplace-ui/src/app/core/services/theme/theme.service.ts create mode 100644 marketplace-ui/src/app/modules/home/home.component.html create mode 100644 marketplace-ui/src/app/modules/home/home.component.scss create mode 100644 marketplace-ui/src/app/modules/home/home.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/home/home.component.ts create mode 100644 marketplace-ui/src/app/modules/home/home.routes.ts create mode 100644 marketplace-ui/src/app/modules/product/product-card/product-card.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-card/product-card.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-card/product-card.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product.component.html create mode 100644 marketplace-ui/src/app/modules/product/product.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product.routes.ts create mode 100644 marketplace-ui/src/app/modules/product/product.service.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product.service.ts create mode 100644 marketplace-ui/src/app/shared/components/footer/footer.component.html create mode 100644 marketplace-ui/src/app/shared/components/footer/footer.component.scss create mode 100644 marketplace-ui/src/app/shared/components/footer/footer.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/footer/footer.component.ts create mode 100644 marketplace-ui/src/app/shared/components/header/header.component.html create mode 100644 marketplace-ui/src/app/shared/components/header/header.component.scss create mode 100644 marketplace-ui/src/app/shared/components/header/header.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/header/header.component.ts create mode 100644 marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.html create mode 100644 marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.scss create mode 100644 marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.ts create mode 100644 marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html create mode 100644 marketplace-ui/src/app/shared/components/header/navigation/navigation.component.scss create mode 100644 marketplace-ui/src/app/shared/components/header/navigation/navigation.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts create mode 100644 marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html create mode 100644 marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.scss create mode 100644 marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts create mode 100644 marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.html create mode 100644 marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.scss create mode 100644 marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.ts create mode 100644 marketplace-ui/src/app/shared/constants/common.constant.ts create mode 100644 marketplace-ui/src/app/shared/enums/language.enum.ts create mode 100644 marketplace-ui/src/app/shared/enums/request-param.ts create mode 100644 marketplace-ui/src/app/shared/enums/sort-option.enum.ts create mode 100644 marketplace-ui/src/app/shared/enums/theme.enum.ts create mode 100644 marketplace-ui/src/app/shared/enums/type-option.enum.ts create mode 100644 marketplace-ui/src/app/shared/mocks/mock-data.ts create mode 100644 marketplace-ui/src/app/shared/mocks/mock-services.ts create mode 100644 marketplace-ui/src/app/shared/models/apis/link.model.ts create mode 100644 marketplace-ui/src/app/shared/models/apis/page.model.ts create mode 100644 marketplace-ui/src/app/shared/models/apis/product-response.model.ts create mode 100644 marketplace-ui/src/app/shared/models/criteria.model.ts create mode 100644 marketplace-ui/src/app/shared/models/display-value.model.ts create mode 100644 marketplace-ui/src/app/shared/models/maven-artifact.model.ts create mode 100644 marketplace-ui/src/app/shared/models/nav-item.model.ts create mode 100644 marketplace-ui/src/app/shared/models/product.model.ts create mode 100644 marketplace-ui/src/app/shared/models/vesion-artifact.model.ts create mode 100644 marketplace-ui/src/app/shared/pipes/logo.pipe.ts create mode 100644 marketplace-ui/src/app/shared/pipes/multilingualism.pipe.ts create mode 100644 marketplace-ui/src/assets/.gitkeep create mode 100644 marketplace-ui/src/assets/fonts/inter.ttf create mode 100644 marketplace-ui/src/assets/i18n/de.yaml create mode 100644 marketplace-ui/src/assets/i18n/en.yaml create mode 100644 marketplace-ui/src/assets/images/misc/axonivy-logo-black.svg create mode 100644 marketplace-ui/src/assets/images/misc/axonivy-logo-round.png create mode 100644 marketplace-ui/src/assets/images/misc/axonivy-logo.svg create mode 100644 marketplace-ui/src/assets/scss/custom-style.scss create mode 100644 marketplace-ui/src/environments/environment.development.ts create mode 100644 marketplace-ui/src/environments/environment.ts create mode 100644 marketplace-ui/src/favicon.ico create mode 100644 marketplace-ui/src/index.html create mode 100644 marketplace-ui/src/main.ts create mode 100644 marketplace-ui/src/styles.scss create mode 100644 marketplace-ui/tsconfig.app.json create mode 100644 marketplace-ui/tsconfig.json create mode 100644 marketplace-ui/tsconfig.spec.json delete mode 100644 src/main/resources/github.token diff --git a/.github/workflows/service-ci-build.yml b/.github/workflows/service-ci-build.yml new file mode 100644 index 000000000..86a8ff9ed --- /dev/null +++ b/.github/workflows/service-ci-build.yml @@ -0,0 +1,50 @@ +name: CI Build +run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} + +on: + push: + workflow_dispatch: + +jobs: + build: + name: Executes Tests + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Tests with Maven + run: mvn clean install + analysis: + name: Sonarqube analysis + needs: build + runs-on: self-hosted + env: + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_PROJECT_KEY : ${{ secrets.SONAR_PROJECT_KEY }} + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Run SonarQube Scanner + run: | + mvn -B verify sonar:sonar \ + -Dsonar.host.url=${{ env.SONAR_HOST_URL }} \ + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} \ + -Dsonar.projectName="AxonIvy Market Service" \ + -Dsonar.token=${{ env.SONAR_TOKEN }} \ + - name: SonarQube Quality Gate check + id: sonarqube-quality-gate-check + uses: sonarsource/sonarqube-quality-gate-action@master + timeout-minutes: 5 + with: + scanMetadataReportFile: target/sonar/report-task.txt + args: -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} diff --git a/.github/workflows/dev-build.yml b/.github/workflows/service-dev-build.yml similarity index 100% rename from .github/workflows/dev-build.yml rename to .github/workflows/service-dev-build.yml diff --git a/.github/workflows/ui-ci-build.yml b/.github/workflows/ui-ci-build.yml new file mode 100644 index 000000000..8a437d1f4 --- /dev/null +++ b/.github/workflows/ui-ci-build.yml @@ -0,0 +1,57 @@ +name: CI Build +run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} + +on: + push: + branches-ignore: + - develop + - master + workflow_dispatch: + +jobs: + build: + name: Build + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - name: Install Dependencies + run: npm install + - name: Build project + run: npm run build + + analysis: + name: Sonarqube + needs: build + runs-on: self-hosted + env: + SONAR_PROJECT_KEY: 'AxonIvy-Market-UI' + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + steps: + - name: Execute Tests + run: npm run test + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + with: + args: + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + - name: SonarQube Quality Gate check + id: sonarqube-quality-gate-check + uses: sonarsource/sonarqube-quality-gate-action@master + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + + - name: Clean up + run: | + rm -rf * diff --git a/.github/workflows/ui-dev-build.yml b/.github/workflows/ui-dev-build.yml new file mode 100644 index 000000000..cb34d8665 --- /dev/null +++ b/.github/workflows/ui-dev-build.yml @@ -0,0 +1,28 @@ +name: Dev Build +run-name: Build and Deploy Marketplace-UI on branch ${{github.ref_name}} by ${{github.actor}} + +on: + push: + branches: [ "develop" ] + workflow_dispatch: + +jobs: + build: + name: Build and deploy new code to Deployment directory + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + - name: Install Dependencies + run: npm install + - name: Build Angular app + run: npm run build -- --configuration production --output-path=dist + - name: Execute Tests + run: npm run test + - name: Copy files to Deployment directory + if: success() + run: sudo cp -r dist/* /var/www/marketplace-ui diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..ec0fe32aa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,24 @@ +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. +As part of the Ricoh Group, Axon Ivy is guided by [The spirit of the three loves](https://www.ricoh.com/about/ricoh-way): + +- **Love your neighbor** 🤝 +We love to get in touch with people and are willing to help others when we are aware of their issues and ideas. Everyone who participates as a user or contributor in this repository is our neighbor. + +- **Love your country** 🗺 +We love the place we’re located at and enjoy the nature around us. We take care of the environment and are eager to learn from cultures around the globe. + +- **Love your work** 👷‍♂️ +We are passionate developers, eager to work with new technologies, and are happy to be part of the digital transformation. We love to be creative at work and see our visions accomplished. + +## Our Guidelines + +This repository is intended to facilitate a friendly and inspiring exchange in which we focus on technical content. + +- Be friendly and patient. +- Be welcoming. +- Be considerate. +- Be respectful. +- Be careful in the words that you choose. +- When we disagree, try to understand why. diff --git a/marketplace-build/Dockerfile b/marketplace-build/Dockerfile new file mode 100644 index 000000000..f6e0339af --- /dev/null +++ b/marketplace-build/Dockerfile @@ -0,0 +1 @@ +Placeholder \ No newline at end of file diff --git a/marketplace-build/Marketplace Tomcat v10.1 Server.launch b/marketplace-build/Marketplace Tomcat v10.1 Server.launch new file mode 100644 index 000000000..5e4b64925 --- /dev/null +++ b/marketplace-build/Marketplace Tomcat v10.1 Server.launch @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/marketplace-build/docker-compose.yml b/marketplace-build/docker-compose.yml new file mode 100644 index 000000000..f6e0339af --- /dev/null +++ b/marketplace-build/docker-compose.yml @@ -0,0 +1 @@ +Placeholder \ No newline at end of file diff --git a/marketplace-service/.gitignore b/marketplace-service/.gitignore new file mode 100644 index 000000000..3ccbd3e32 --- /dev/null +++ b/marketplace-service/.gitignore @@ -0,0 +1,36 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +/bin/ + +### Token +*.token diff --git a/marketplace-service/README.md b/marketplace-service/README.md new file mode 100644 index 000000000..4d546f33a --- /dev/null +++ b/marketplace-service/README.md @@ -0,0 +1,34 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.2.5/maven-plugin/reference/html/) +* [Spring Data MongoDB](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#data.nosql.mongodb) +* [Spring Web](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#web) + +### Guides +The following guides illustrate how to use some features concretely: + +* Installing mongodb, and access it as Url mongodb://localhost:27017/, and you can create and name whatever you want ,then you should put them to application.properties +* You can change the MongoDB configuration in file `application.properties` + ``` + spring.data.mongodb.host= + spring.data.mongodb.database= + ``` +* Update GitHub token in file `github.token` +* Run mvn clean install to build project +* Run mvn test to test all tests + + +### Access Swagger URL: http://{your-host}/swagger-ui/index.html + +### Install Lombok for Eclipse IDE +* Download lombok here https://projectlombok.org/download +* run command "java -jar lombok.jar" then you can access file “eclipse.ini“ in eclipse folder where you install → there is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are successful +* Start eclipse +* Import the project then in the eclipse , you should run the command “mvn clean install“ +* After that you go to class MarketplaceServiceApplication → right click to main method → click run as → choose Java Application +* Then you can send a request in postman +* If you want to run single test in class UserServiceImplTest. You can right-click to method testFindAllUser and right click → select Run as → choose JUnit Test \ No newline at end of file diff --git a/lombok.config b/marketplace-service/lombok.config similarity index 100% rename from lombok.config rename to marketplace-service/lombok.config diff --git a/pom.xml b/marketplace-service/pom.xml similarity index 100% rename from pom.xml rename to marketplace-service/pom.xml diff --git a/sonar-project.properties b/marketplace-service/sonar-project.properties similarity index 100% rename from sonar-project.properties rename to marketplace-service/sonar-project.properties diff --git a/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java similarity index 100% rename from src/main/java/com/axonivy/market/MarketplaceServiceApplication.java rename to marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java diff --git a/src/main/java/com/axonivy/market/ServletInitializer.java b/marketplace-service/src/main/java/com/axonivy/market/ServletInitializer.java similarity index 100% rename from src/main/java/com/axonivy/market/ServletInitializer.java rename to marketplace-service/src/main/java/com/axonivy/market/ServletInitializer.java diff --git a/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java similarity index 100% rename from src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java rename to marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java diff --git a/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java similarity index 100% rename from src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java rename to marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java diff --git a/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java similarity index 100% rename from src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java rename to marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java diff --git a/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java similarity index 100% rename from src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java rename to marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java diff --git a/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java similarity index 100% rename from src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java rename to marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java diff --git a/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java similarity index 100% rename from src/main/java/com/axonivy/market/config/MongoConfig.java rename to marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java diff --git a/src/main/java/com/axonivy/market/config/WebConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java similarity index 100% rename from src/main/java/com/axonivy/market/config/WebConfig.java rename to marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java diff --git a/src/main/java/com/axonivy/market/constants/CommonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/CommonConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java diff --git a/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/EntityConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java diff --git a/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java diff --git a/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/GitHubConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java diff --git a/src/main/java/com/axonivy/market/constants/MavenConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/MavenConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java diff --git a/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java diff --git a/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/ProductJsonConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java diff --git a/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java similarity index 100% rename from src/main/java/com/axonivy/market/constants/RequestMappingConstants.java rename to marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java diff --git a/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java similarity index 100% rename from src/main/java/com/axonivy/market/controller/AppController.java rename to marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java diff --git a/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java similarity index 100% rename from src/main/java/com/axonivy/market/controller/ProductController.java rename to marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java diff --git a/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java similarity index 100% rename from src/main/java/com/axonivy/market/controller/ProductDetailsController.java rename to marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java diff --git a/src/main/java/com/axonivy/market/controller/UserController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java similarity index 100% rename from src/main/java/com/axonivy/market/controller/UserController.java rename to marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java diff --git a/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java similarity index 100% rename from src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java rename to marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java diff --git a/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java similarity index 100% rename from src/main/java/com/axonivy/market/entity/MavenArtifactModel.java rename to marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java diff --git a/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java similarity index 100% rename from src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java rename to marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java diff --git a/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java similarity index 100% rename from src/main/java/com/axonivy/market/entity/Product.java rename to marketplace-service/src/main/java/com/axonivy/market/entity/Product.java diff --git a/src/main/java/com/axonivy/market/entity/User.java b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java similarity index 100% rename from src/main/java/com/axonivy/market/entity/User.java rename to marketplace-service/src/main/java/com/axonivy/market/entity/User.java diff --git a/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java similarity index 100% rename from src/main/java/com/axonivy/market/enums/ErrorCode.java rename to marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java diff --git a/src/main/java/com/axonivy/market/enums/FileStatus.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java similarity index 100% rename from src/main/java/com/axonivy/market/enums/FileStatus.java rename to marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java diff --git a/src/main/java/com/axonivy/market/enums/FileType.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java similarity index 100% rename from src/main/java/com/axonivy/market/enums/FileType.java rename to marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java diff --git a/src/main/java/com/axonivy/market/enums/Language.java b/marketplace-service/src/main/java/com/axonivy/market/enums/Language.java similarity index 100% rename from src/main/java/com/axonivy/market/enums/Language.java rename to marketplace-service/src/main/java/com/axonivy/market/enums/Language.java diff --git a/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java similarity index 100% rename from src/main/java/com/axonivy/market/enums/SortOption.java rename to marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java diff --git a/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java similarity index 100% rename from src/main/java/com/axonivy/market/enums/TypeOption.java rename to marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java diff --git a/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java similarity index 100% rename from src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java rename to marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java diff --git a/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java similarity index 100% rename from src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java rename to marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java diff --git a/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java similarity index 100% rename from src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java rename to marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java diff --git a/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java similarity index 100% rename from src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java rename to marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java diff --git a/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java similarity index 100% rename from src/main/java/com/axonivy/market/factory/ProductFactory.java rename to marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java diff --git a/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java similarity index 100% rename from src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java rename to marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java diff --git a/src/main/java/com/axonivy/market/github/model/GitHubFile.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java similarity index 100% rename from src/main/java/com/axonivy/market/github/model/GitHubFile.java rename to marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java diff --git a/src/main/java/com/axonivy/market/github/model/MavenArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java similarity index 100% rename from src/main/java/com/axonivy/market/github/model/MavenArtifact.java rename to marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java diff --git a/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java similarity index 100% rename from src/main/java/com/axonivy/market/github/model/Meta.java rename to marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java diff --git a/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java similarity index 100% rename from src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java rename to marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java diff --git a/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java similarity index 100% rename from src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java rename to marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java diff --git a/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java similarity index 100% rename from src/main/java/com/axonivy/market/github/service/GitHubService.java rename to marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java diff --git a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java similarity index 100% rename from src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java rename to marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java diff --git a/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java similarity index 100% rename from src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java rename to marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java diff --git a/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java similarity index 100% rename from src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java rename to marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java diff --git a/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java similarity index 100% rename from src/main/java/com/axonivy/market/github/util/GitHubUtils.java rename to marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java diff --git a/src/main/java/com/axonivy/market/model/DisplayValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java similarity index 100% rename from src/main/java/com/axonivy/market/model/DisplayValue.java rename to marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java diff --git a/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java similarity index 100% rename from src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java rename to marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java diff --git a/src/main/java/com/axonivy/market/model/Message.java b/marketplace-service/src/main/java/com/axonivy/market/model/Message.java similarity index 100% rename from src/main/java/com/axonivy/market/model/Message.java rename to marketplace-service/src/main/java/com/axonivy/market/model/Message.java diff --git a/src/main/java/com/axonivy/market/model/MultilingualismValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java similarity index 100% rename from src/main/java/com/axonivy/market/model/MultilingualismValue.java rename to marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java diff --git a/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java similarity index 100% rename from src/main/java/com/axonivy/market/model/ProductModel.java rename to marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java diff --git a/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java similarity index 100% rename from src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java rename to marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java diff --git a/src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java similarity index 100% rename from src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java rename to marketplace-service/src/main/java/com/axonivy/market/repository/MavenArtifactVersionRepository.java diff --git a/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java similarity index 100% rename from src/main/java/com/axonivy/market/repository/ProductRepository.java rename to marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java diff --git a/src/main/java/com/axonivy/market/repository/UserRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java similarity index 100% rename from src/main/java/com/axonivy/market/repository/UserRepository.java rename to marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java diff --git a/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java similarity index 100% rename from src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java rename to marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java diff --git a/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java similarity index 100% rename from src/main/java/com/axonivy/market/service/ProductService.java rename to marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java diff --git a/src/main/java/com/axonivy/market/service/UserService.java b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java similarity index 100% rename from src/main/java/com/axonivy/market/service/UserService.java rename to marketplace-service/src/main/java/com/axonivy/market/service/UserService.java diff --git a/src/main/java/com/axonivy/market/service/VersionService.java b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java similarity index 100% rename from src/main/java/com/axonivy/market/service/VersionService.java rename to marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java diff --git a/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java similarity index 100% rename from src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java rename to marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java diff --git a/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java similarity index 100% rename from src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java rename to marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java diff --git a/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java similarity index 100% rename from src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java rename to marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java diff --git a/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java similarity index 100% rename from src/main/java/com/axonivy/market/utils/XmlReaderUtils.java rename to marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java diff --git a/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties similarity index 100% rename from src/main/resources/application.properties rename to marketplace-service/src/main/resources/application.properties diff --git a/src/test/java/com/axonivy/market/controller/AppControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java similarity index 100% rename from src/test/java/com/axonivy/market/controller/AppControllerTest.java rename to marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java diff --git a/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java similarity index 100% rename from src/test/java/com/axonivy/market/controller/ProductControllerTest.java rename to marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java diff --git a/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java similarity index 100% rename from src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java rename to marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java diff --git a/src/test/java/com/axonivy/market/controller/UserControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java similarity index 100% rename from src/test/java/com/axonivy/market/controller/UserControllerTest.java rename to marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java diff --git a/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java similarity index 100% rename from src/test/java/com/axonivy/market/factory/ProductFactoryTest.java rename to marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java diff --git a/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java similarity index 100% rename from src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java diff --git a/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java similarity index 100% rename from src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java rename to marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java diff --git a/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java similarity index 100% rename from src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java diff --git a/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java similarity index 100% rename from src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java diff --git a/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java similarity index 100% rename from src/test/java/com/axonivy/market/service/ProductServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java diff --git a/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java similarity index 100% rename from src/test/java/com/axonivy/market/service/SchedulingTasksTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java diff --git a/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java similarity index 100% rename from src/test/java/com/axonivy/market/service/UserServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java diff --git a/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java similarity index 100% rename from src/test/java/com/axonivy/market/service/VersionServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java diff --git a/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java similarity index 100% rename from src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java rename to marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java diff --git a/src/test/resources/meta.json b/marketplace-service/src/test/resources/meta.json similarity index 100% rename from src/test/resources/meta.json rename to marketplace-service/src/test/resources/meta.json diff --git a/marketplace-ui/.editorconfig b/marketplace-ui/.editorconfig new file mode 100644 index 000000000..59d9a3a3e --- /dev/null +++ b/marketplace-ui/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/marketplace-ui/.gitignore b/marketplace-ui/.gitignore new file mode 100644 index 000000000..2e11c9dab --- /dev/null +++ b/marketplace-ui/.gitignore @@ -0,0 +1,48 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +# Coverage +/coverage +/reports + +**/assets/market-cache diff --git a/marketplace-ui/.prettierrc b/marketplace-ui/.prettierrc new file mode 100644 index 000000000..035febea0 --- /dev/null +++ b/marketplace-ui/.prettierrc @@ -0,0 +1,15 @@ +{ + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ], + "singleQuote": true, + "htmlWhitespaceSensitivity": "ignore", + "bracketSameLine": true, + "trailingComma": "none", + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/marketplace-ui/.vscode/extensions.json b/marketplace-ui/.vscode/extensions.json new file mode 100644 index 000000000..77b374577 --- /dev/null +++ b/marketplace-ui/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/marketplace-ui/.vscode/launch.json b/marketplace-ui/.vscode/launch.json new file mode 100644 index 000000000..2c137b20d --- /dev/null +++ b/marketplace-ui/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Chrome Debug", + "request": "launch", + "type": "chrome", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/marketplace-ui/.vscode/tasks.json b/marketplace-ui/.vscode/tasks.json new file mode 100644 index 000000000..a298b5bd8 --- /dev/null +++ b/marketplace-ui/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/marketplace-ui/README.md b/marketplace-ui/README.md new file mode 100644 index 000000000..9237bee55 --- /dev/null +++ b/marketplace-ui/README.md @@ -0,0 +1,27 @@ +# MarketplaceUi + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/marketplace-ui/angular.json b/marketplace-ui/angular.json new file mode 100644 index 000000000..7b92f41be --- /dev/null +++ b/marketplace-ui/angular.json @@ -0,0 +1,124 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "marketplace-ui": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets", + "src/assets/_market" + ], + "styles": [ + "node_modules/bootstrap/dist/css/bootstrap.min.css", + "src/styles.scss", + "node_modules/@fortawesome/fontawesome-free/css/all.min.css" + ], + "scripts": [ + "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" + ] + }, + "configurations": { + "production": { + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + }, + "fonts": true + }, + "budgets": [ + { + "type": "initial", + "maximumWarning": "1mb", + "maximumError": "5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "marketplace-ui:build:production" + }, + "development": { + "buildTarget": "marketplace-ui:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "marketplace-ui:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "codeCoverage": true, + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [], + "karmaConfig": "karma.conf.js", + "sourceMap": true, + "watch": false + } + } + } + } + }, + "cli": { + "analytics": false + } +} \ No newline at end of file diff --git a/marketplace-ui/karma.conf.js b/marketplace-ui/karma.conf.js new file mode 100644 index 000000000..4a568c025 --- /dev/null +++ b/marketplace-ui/karma.conf.js @@ -0,0 +1,58 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular', 'viewport'], + plugins: [ + require('karma-jasmine'), + require('karma-webpack'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + require('karma-viewport') + ], + + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + stopSpecOnExpectationFailure: true, + failFast: true, + timeoutInterval: 60000, + random: false + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'lcov' }] + }, + preprocessors: { + // source files, that you wanna generate coverage for + // do not include tests or libraries + // (these files will be instrumented by Istanbul) + 'src/**/mocks/**': ['coverage'] + }, + angularCli: { + environment: 'dev' + }, + reporters: ['progress', 'coverage'], + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, + restartOnFileChange: true + }); +}; diff --git a/marketplace-ui/package-lock.json b/marketplace-ui/package-lock.json new file mode 100644 index 000000000..dc4bce9eb --- /dev/null +++ b/marketplace-ui/package-lock.json @@ -0,0 +1,14063 @@ +{ + "name": "marketplace-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "marketplace-ui", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^18.0.0", + "@angular/common": "^18.0.0", + "@angular/compiler": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "@angular/platform-browser": "^18.0.0", + "@angular/platform-browser-dynamic": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/router": "^18.0.0", + "@fortawesome/fontawesome-free": "^6.5.2", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "karma-viewport": "^1.0.9", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "yaml": "^2.4.2", + "zone.js": "~0.14.3" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.0.0", + "@angular/cli": "^18.0.0", + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@types/bootstrap": "^5.2.10", + "@types/jasmine": "~5.1.0", + "@types/node": "^18.18.0", + "jasmine": "^5.1.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "karma-jsdom-launcher": "^17.0.0", + "karma-junit-reporter": "^2.0.1", + "karma-sonarqube-reporter": "^1.4.0", + "karma-webpack": "^5.0.1", + "prettier": "^3.2.5", + "typescript": "~5.4.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1800.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1800.0.tgz", + "integrity": "sha512-B28h/+Og1F8/QWlizmOl3Iv3svH9uIJ456gw331RgtUMrYszU6WPlk1izG38PV++NKK9vv9NcqQsJCEvxY9ipg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.0.0", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.0.0.tgz", + "integrity": "sha512-EZDn/2h24mldx8c8zbJ5BAz8YmXmPhdbFOILPixsTInJJ9/iKX+cFioyscqzRDkVuISMA8AagC+5E2ZIhCjiPQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1800.0", + "@angular-devkit/build-webpack": "0.1800.0", + "@angular-devkit/core": "18.0.0", + "@angular/build": "18.0.0", + "@babel/core": "7.24.5", + "@babel/generator": "7.24.5", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.24.5", + "@babel/plugin-transform-async-generator-functions": "7.24.3", + "@babel/plugin-transform-async-to-generator": "7.24.1", + "@babel/plugin-transform-runtime": "7.24.3", + "@babel/preset-env": "7.24.5", + "@babel/runtime": "7.24.5", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "18.0.0", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.19", + "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": "7.1.1", + "esbuild-wasm": "0.21.3", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.0", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.22", + "jsonc-parser": "3.2.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.10", + "mini-css-extract-plugin": "2.9.0", + "mrmime": "2.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.5.0", + "postcss": "8.4.38", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.77.2", + "sass-loader": "14.2.1", + "semver": "7.6.2", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.31.0", + "tree-kill": "1.2.2", + "tslib": "2.6.2", + "undici": "6.18.0", + "vite": "5.2.11", + "watchpack": "2.4.1", + "webpack": "5.91.0", + "webpack-dev-middleware": "7.2.1", + "webpack-dev-server": "5.0.4", + "webpack-merge": "5.10.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.21.3" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.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": "^18.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <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/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "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-devkit/build-angular/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==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular-devkit/build-angular/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 + }, + "node_modules/@angular-devkit/build-angular/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1800.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1800.0.tgz", + "integrity": "sha512-L61mW+aGK+opsokUZkj7q1/gnSyF3qz+FsAqdVyTvwBta3KKr8xzNR75fwvzZ9+qD8bum5oAOgtyw+tvPMMt3g==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1800.0", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.0.0.tgz", + "integrity": "sha512-mFD4QgyM1SwPjk6slJsqAXX7oTNduYbA5zgyf29/9wNUagUaz0vdonwxFlHv+D5pPmX/tRY5mqxYD68F7FiC9g==", + "dev": true, + "dependencies": { + "ajv": "8.13.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.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/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.0.0.tgz", + "integrity": "sha512-whvMDjnLd5ObyfO+HGZdPMtY8Ac+kVyVq2RigpKQmOoQOk8eMZw4iRsTOGzvaKXhFcFnTbT5O3c6Pvo42aCaAA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.0.0", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.10", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/animations": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.0.0.tgz", + "integrity": "sha512-An/IqDBCyWZXVC23+jRKdmvJB/b4P1BVljZxGxF+CiocNd/xvVVeBYuuxzp3vhhVobyO8A9iD12itPudLOpt2Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "18.0.0" + } + }, + "node_modules/@angular/build": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.0.0.tgz", + "integrity": "sha512-CVE/08mH7LhcHte0UN9ETZ+d7ewPPLbtdMXYnCNvbbAqfOCaPQ62agDzBE9sHOLlyn6fkFX2G4mwyKV+AQbQnw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1800.0", + "@babel/core": "7.24.5", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.24.5", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "browserslist": "^4.23.0", + "critters": "0.0.22", + "esbuild": "0.21.3", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.22", + "lmdb": "3.0.8", + "magic-string": "0.30.10", + "mrmime": "2.0.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.5.0", + "sass": "1.77.2", + "semver": "7.6.2", + "undici": "6.18.0", + "vite": "5.2.11", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.5" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "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/build/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==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/build/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 + }, + "node_modules/@angular/build/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.0.0.tgz", + "integrity": "sha512-SzPMju4L7Lr59k72PNmEznCSfHGtoDSmDl3lbLoumnIKlZoejnIgEipzXSjTkBk23rHAAUevlpDUUhkOIoAppg==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1800.0", + "@angular-devkit/core": "18.0.0", + "@angular-devkit/schematics": "18.0.0", + "@schematics/angular": "18.0.0", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.2", + "inquirer": "9.2.22", + "jsonc-parser": "3.2.1", + "npm-package-arg": "11.0.2", + "npm-pick-manifest": "9.0.1", + "ora": "5.4.1", + "pacote": "18.0.6", + "resolve": "1.22.8", + "semver": "7.6.2", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/common": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.0.0.tgz", + "integrity": "sha512-s43ZcOhXTUlkdOPMiMtr4Pz1qKIS8nClXhaahY0JBQZYGsOSn7NR42SoEeB8/ixktfY60s3SLhizXTKMAYtOTA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.0.0.tgz", + "integrity": "sha512-KbyjUfpdVE8+6fiHqo4PgVrGppYUhlU1JVAj6dqeUug9lQ5HBcANfiZ7p8CA2lU3gvIZ1cj+ZDKA1NEB1wvvtQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "18.0.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.0.0.tgz", + "integrity": "sha512-fy9MBSHDM/YAyrIWa15JV1ZrpuSc51HHUSA3W/UKrDqUqSfYyj11/0PeYkdIWUD/dACZSrEge3nVnYCjdyJqPA==", + "dev": true, + "dependencies": { + "@babel/core": "7.24.4", + "@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": "18.0.0", + "typescript": ">=5.4 <5.5" + } + }, + "node_modules/@angular/core": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.0.0.tgz", + "integrity": "sha512-tpR7HIY4MJuM9ETpG15IvBr1wsI8Cyec3ZxYFe/27FKHARvxDbqIrT9QevmC6lxg1NdfD990G2XphYML1EyJ8g==", + "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": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.0.0.tgz", + "integrity": "sha512-Q+4WExdgALP7VJ5lKSYmpz8CtAFZI4f3n09JhExIZoPTLD/mqOJcxxO7wTc9lXG4jKSE8BlfgK2txKz1cQvrEQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "18.0.0", + "@angular/core": "18.0.0", + "@angular/platform-browser": "18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/localize": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.0.0.tgz", + "integrity": "sha512-DW3wB5Cj0a+Ph5SppddRcXTH6igX+W5x7wK+VDsLefiAC2cHRG4DjEL2mpoVYrkDUPNQRaf+X4GTEKHtTzjvNw==", + "dev": true, + "dependencies": { + "@babel/core": "7.24.4", + "@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": "18.0.0", + "@angular/compiler-cli": "18.0.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.0.0.tgz", + "integrity": "sha512-fOqXQn15H33xGTGgNBUwXAg5KRpqcdsVfipFBuD1GMbjMLQAx/AagxsBavRiq3mKEdHZyQ+hI4mvaKQWOPKUOQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "18.0.0", + "@angular/common": "18.0.0", + "@angular/core": "18.0.0" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.0.0.tgz", + "integrity": "sha512-Z7Y2qzEuFgCrkgcKPuyHGStEnZ89L3gr3SIgqoVlz4kauf0Fa70H6dxyd/RXV61OZwLXx0yt9rV5d8v+Ay+3fQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "18.0.0", + "@angular/compiler": "18.0.0", + "@angular/core": "18.0.0", + "@angular/platform-browser": "18.0.0" + } + }, + "node_modules/@angular/platform-server": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-18.0.0.tgz", + "integrity": "sha512-xn/E1zYEWnvoeSGDcMjxOmUhOIkTQ4wSmoAEr3lNt8znB/+K3PnMsV6sHPSgOkfjzXuX7PFhW2tgvp4TbMgfbA==", + "dependencies": { + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "18.0.0", + "@angular/common": "18.0.0", + "@angular/compiler": "18.0.0", + "@angular/core": "18.0.0", + "@angular/platform-browser": "18.0.0" + } + }, + "node_modules/@angular/router": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.0.0.tgz", + "integrity": "sha512-bytfTypkJbHDv2QkD8jT2w63DWKicSYi5l7N+LPukb9/0pl3XYXKJ8cjlVLbiFvoo5Oz2oBFWYFucWsaPqDw3A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "18.0.0", + "@angular/core": "18.0.0", + "@angular/platform-browser": "18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@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==", + "dev": true + }, + "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==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "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, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "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==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "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, + "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.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", + "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.23.0", + "@babel/template": "^7.24.0", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz", + "integrity": "sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.1", + "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.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "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, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-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, + "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, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-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, + "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, + "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-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, + "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.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz", + "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@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.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@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.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", + "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", + "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.24.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", + "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", + "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", + "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", + "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "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.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz", + "integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-plugin-utils": "^7.24.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", + "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.5.tgz", + "integrity": "sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", + "@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.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", + "@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.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.5", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.4", + "@babel/plugin-transform-classes": "^7.24.5", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.5", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.5", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.5", + "@babel/plugin-transform-parameters": "^7.24.5", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.5", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.5", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "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, + "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, + "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/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "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, + "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, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.3.tgz", + "integrity": "sha512-yTgnwQpFVYfvvo4SvRFB0SwrW8YjOxEoT7wfMT7Ol5v7v5LDNvSGo67aExmxOb87nQNeWPVvaGBNfQ7BXcrZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.3.tgz", + "integrity": "sha512-bviJOLMgurLJtF1/mAoJLxDZDL6oU5/ztMHnJQRejbJrSc9FFu0QoUoFhvi6qSKJEw9y5oGyvr9fuDtzJ30rNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.3.tgz", + "integrity": "sha512-c+ty9necz3zB1Y+d/N+mC6KVVkGUUOcm4ZmT5i/Fk5arOaY3i6CA3P5wo/7+XzV8cb4GrI/Zjp8NuOQ9Lfsosw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.3.tgz", + "integrity": "sha512-JReHfYCRK3FVX4Ra+y5EBH1b9e16TV2OxrPAvzMsGeES0X2Ndm9ImQRI4Ket757vhc5XBOuGperw63upesclRw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.3.tgz", + "integrity": "sha512-U3fuQ0xNiAkXOmQ6w5dKpEvXQRSpHOnbw7gEfHCRXPeTKW9sBzVck6C5Yneb8LfJm0l6le4NQfkNPnWMSlTFUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.3.tgz", + "integrity": "sha512-3m1CEB7F07s19wmaMNI2KANLcnaqryJxO1fXHUV5j1rWn+wMxdUYoPyO2TnAbfRZdi7ADRwJClmOwgT13qlP3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.3.tgz", + "integrity": "sha512-fsNAAl5pU6wmKHq91cHWQT0Fz0vtyE1JauMzKotrwqIKAswwP5cpHUCxZNSTuA/JlqtScq20/5KZ+TxQdovU/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.3.tgz", + "integrity": "sha512-tci+UJ4zP5EGF4rp8XlZIdq1q1a/1h9XuronfxTMCNBslpCtmk97Q/5qqy1Mu4zIc0yswN/yP/BLX+NTUC1bXA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.3.tgz", + "integrity": "sha512-f6kz2QpSuyHHg01cDawj0vkyMwuIvN62UAguQfnNVzbge2uWLhA7TCXOn83DT0ZvyJmBI943MItgTovUob36SQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.3.tgz", + "integrity": "sha512-vvG6R5g5ieB4eCJBQevyDMb31LMHthLpXTc2IGkFnPWS/GzIFDnaYFp558O+XybTmYrVjxnryru7QRleJvmZ6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.3.tgz", + "integrity": "sha512-HjCWhH7K96Na+66TacDLJmOI9R8iDWDDiqe17C7znGvvE4sW1ECt9ly0AJ3dJH62jHyVqW9xpxZEU1jKdt+29A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.3.tgz", + "integrity": "sha512-BGpimEccmHBZRcAhdlRIxMp7x9PyJxUtj7apL2IuoG9VxvU/l/v1z015nFs7Si7tXUwEsvjc1rOJdZCn4QTU+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.3.tgz", + "integrity": "sha512-5rMOWkp7FQGtAH3QJddP4w3s47iT20hwftqdm7b+loe95o8JU8ro3qZbhgMRy0VuFU0DizymF1pBKkn3YHWtsw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.3.tgz", + "integrity": "sha512-h0zj1ldel89V5sjPLo5H1SyMzp4VrgN1tPkN29TmjvO1/r0MuMRwJxL8QY05SmfsZRs6TF0c/IDH3u7XYYmbAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.3.tgz", + "integrity": "sha512-dkAKcTsTJ+CRX6bnO17qDJbLoW37npd5gSNtSzjYQr0svghLJYGYB0NF1SNcU1vDcjXLYS5pO4qOW4YbFama4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.3.tgz", + "integrity": "sha512-vnD1YUkovEdnZWEuMmy2X2JmzsHQqPpZElXx6dxENcIwTu+Cu5ERax6+Ke1QsE814Zf3c6rxCfwQdCTQ7tPuXA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.3.tgz", + "integrity": "sha512-IOXOIm9WaK7plL2gMhsWJd+l2bfrhfilv0uPTptoRoSb2p09RghhQQp9YY6ZJhk/kqmeRt6siRdMSLLwzuT0KQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.3.tgz", + "integrity": "sha512-uTgCwsvQ5+vCQnqM//EfDSuomo2LhdWhFPS8VL8xKf+PKTCrcT/2kPPoWMTs22aB63MLdGMJiE3f1PHvCDmUOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.3.tgz", + "integrity": "sha512-vNAkR17Ub2MgEud2Wag/OE4HTSI6zlb291UYzHez/psiKarp0J8PKGDnAhMBcHFoOHMXHfExzmjMojJNbAStrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.3.tgz", + "integrity": "sha512-W8H9jlGiSBomkgmouaRoTXo49j4w4Kfbl6I1bIdO/vT0+0u4f20ko3ELzV3hPI6XV6JNBVX+8BC+ajHkvffIJA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.3.tgz", + "integrity": "sha512-EjEomwyLSCg8Ag3LDILIqYCZAq/y3diJ04PnqGRgq8/4O3VNlXyMd54j/saShaN4h5o5mivOjAzmU6C3X4v0xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.3.tgz", + "integrity": "sha512-WGiE/GgbsEwR33++5rzjiYsKyHywE8QSZPF7Rfx9EBfK3Qn3xyR6IjyCr5Uk38Kg8fG4/2phN7sXp4NPWd3fcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.3.tgz", + "integrity": "sha512-xRxC0jaJWDLYvcUvjQmHCJSfMrgmUuvsoXgDeU/wTorQ1ngDdUBuFtgY3W1Pc5sprGAvZBtWdJX7RPg/iZZUqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz", + "integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.2.tgz", + "integrity": "sha512-4F1MBwVr3c/m4bAUef6LgkvBfSjzwH+OfldgHqcuacWwSUetFebM2wi58WfG9uk1rR98U6GwLed4asLJbwdV5w==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "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, + "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.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "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, + "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 + }, + "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, + "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, + "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, + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz", + "integrity": "sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==", + "dev": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.1.3.tgz", + "integrity": "sha512-g//kkF4kOwUjemValCtOc/xiYzmwMRmWq3Bn+YnzOzuZLHq2PpMOxxIayN3cKbo7Ko2Np65t6D9H81IvXbXhqg==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "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 + }, + "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, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.8.tgz", + "integrity": "sha512-+lFwFvU+zQ9zVIFETNtmW++syh3Ps5JS8MPQ8zOYtQZoU+dTR8ivWHTaE2QVk1JG2payGDLUAvpndLAjGMdeeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.8.tgz", + "integrity": "sha512-T98rfsgfdQMS5/mqdsPb6oHSJ+iBYNa+PQDLtXLh6rzTEBsYP9x2uXxIj6VS4qXVDWXVi8rv85NCOG+UBOsHXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.8.tgz", + "integrity": "sha512-gVNCi3bYWatdPMeFpFjuZl6bzVL55FkeZU3sPeU+NsMRXC+Zl3qOx3M6cM4OMlJWbhHjYjf2b8q83K0mczaiWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.8.tgz", + "integrity": "sha512-uEBGCQIChsixpykL0pjCxfF64btv64vzsb1NoM5u0qvabKvKEvErhXGoqovyldDu9u1T/fswD8Kf6ih0vJEvDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.8.tgz", + "integrity": "sha512-6v0B4sa9ulNezmDZtVpLjNHmA0qZzUl3001YJ2RF0naxsuv/Jq/xEwNYpOzfcdizHfpCE0oBkWzk/r+Slr+0zw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.8.tgz", + "integrity": "sha512-lDLGRIMqdwYD39vinwNqqZUxCdL2m2iIdn+0HyQgIHEiT0g5rIAlzaMKzoGWon5NQumfxXFk9y0DarttkR7C1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", + "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", + "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", + "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", + "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", + "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", + "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ngtools/webpack": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.0.tgz", + "integrity": "sha512-wcJp15H52RgEiZOcq/8YlgF53dNR2C+ap6mof8HziD5lTXmkPLKn1US0gqHixu5njkWKslCzu2td/k2Fg6r5Kg==", + "dev": true, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <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==", + "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==", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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, + "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/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "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, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", + "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "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/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, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "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, + "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, + "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/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, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.0.tgz", + "integrity": "sha512-1aL4TuVrLS9sf8quCLerU3H9J4vtCtgu8VauYozrmEyU57i/EdKleCnsQ7vpnABIH6c9mnTxcH5sFkO3BlV8wQ==", + "dev": true, + "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/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "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, + "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, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.0.tgz", + "integrity": "sha512-SEjCPAVHWYUIQR+Yn03kJmrJjZDtJLYpj300m3HV9OTRZNpC5YpbMsM3eTkECyT4aWj8lDr9WeY6TWefpubtYQ==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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, + "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/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, + "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, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.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, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.0.0.tgz", + "integrity": "sha512-cFah74mKIg+mCGur1Q1BmsQ/u+Ne/0MOwIxe2oYSlzDpktOuKAUItPFe4GHxm9Mu5qZzOX0Z4RRnSojU8XgZEw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.0.0", + "@angular-devkit/schematics": "18.0.0", + "jsonc-parser": "3.2.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "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, + "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, + "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, + "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, + "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/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "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, + "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/@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 + }, + "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, + "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, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "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, + "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, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bootstrap": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", + "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, + "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, + "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, + "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 + }, + "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, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "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, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "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, + "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": "4.19.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.1.tgz", + "integrity": "sha512-ej0phymbFLoCB26dbbq5PGScsf2JAJ4IJHjG10LalgUV36XKTmA4GdA+PVllKvRk0sEKt64X8975qFnkSi0hqA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "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 + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", + "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", + "dev": true + }, + "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 + }, + "node_modules/@types/karma": { + "version": "6.3.8", + "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.8.tgz", + "integrity": "sha512-+QGoOPhb1f6Oli8pG+hxdnGDzVhIrpsHaFSJ4UJg15Xj+QBtluKELkJY+L4Li532HmT3l5K5o1FoUZHRQeOOaQ==", + "dependencies": { + "@types/node": "*", + "log4js": "^6.4.1" + } + }, + "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 + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/mkdirp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz", + "integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", + "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", + "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, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "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 + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, + "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, + "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, + "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, + "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, + "dependencies": { + "@types/node": "*" + } + }, + "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 + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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, + "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, + "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 + }, + "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 + }, + "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 + }, + "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, + "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 + }, + "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, + "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, + "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, + "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 + }, + "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, + "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, + "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, + "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, + "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, + "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 + }, + "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 + }, + "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 + }, + "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, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "peerDependencies": { + "acorn": "^8" + } + }, + "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, + "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, + "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, + "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, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "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, + "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, + "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, + "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, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "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" + ], + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/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 + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "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, + "peer": true + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "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" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "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/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, + "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, + "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-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, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.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 + }, + "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" + } + ] + }, + "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, + "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 + }, + "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, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "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.11.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, + "dependencies": { + "ms": "2.0.0" + } + }, + "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 + }, + "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, + "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 + }, + "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" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/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 + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", + "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "dev": true, + "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/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/cacache/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001621", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", + "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", + "dev": true, + "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" + } + ] + }, + "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==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "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, + "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, + "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, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/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, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-regexp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", + "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "dev": true, + "dependencies": { + "is-regexp": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true + }, + "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, + "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 + }, + "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, + "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, + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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 + }, + "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 + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "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, + "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, + "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, + "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 + }, + "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 + }, + "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 + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "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, + "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, + "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 + }, + "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, + "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, + "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==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "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 + }, + "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, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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, + "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/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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "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 + }, + "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, + "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, + "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 + }, + "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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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, + "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, + "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, + "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, + "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 + }, + "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, + "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, + "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, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.1.tgz", + "integrity": "sha512-OxIR5P2mjO1PSXk44bWuQ8XtMK4dpEqpIyERCx3ewOo3I8EmbcxMPUc5ScLtQfgXtOojoMv57So4V/C02HQLsw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.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", + "webpack": "^5.27.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, + "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, + "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, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "peer": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "peer": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "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, + "peer": true + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.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, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "peer": true, + "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, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "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 + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "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, + "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, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.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, + "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" + } + ] + }, + "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, + "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, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "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 + }, + "node_modules/electron-to-chromium": { + "version": "1.4.777", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.777.tgz", + "integrity": "sha512-n02NCwLJ3wexLfK/yQeqfywCblZqLcXphzmid5e8yVPdtEcida7li0A5WQKghHNG0FeOMCzeFOzEbtAh5riXFw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "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 + }, + "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, + "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, + "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, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "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.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", + "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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 + }, + "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, + "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, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.3.tgz", + "integrity": "sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.3", + "@esbuild/android-arm": "0.21.3", + "@esbuild/android-arm64": "0.21.3", + "@esbuild/android-x64": "0.21.3", + "@esbuild/darwin-arm64": "0.21.3", + "@esbuild/darwin-x64": "0.21.3", + "@esbuild/freebsd-arm64": "0.21.3", + "@esbuild/freebsd-x64": "0.21.3", + "@esbuild/linux-arm": "0.21.3", + "@esbuild/linux-arm64": "0.21.3", + "@esbuild/linux-ia32": "0.21.3", + "@esbuild/linux-loong64": "0.21.3", + "@esbuild/linux-mips64el": "0.21.3", + "@esbuild/linux-ppc64": "0.21.3", + "@esbuild/linux-riscv64": "0.21.3", + "@esbuild/linux-s390x": "0.21.3", + "@esbuild/linux-x64": "0.21.3", + "@esbuild/netbsd-x64": "0.21.3", + "@esbuild/openbsd-x64": "0.21.3", + "@esbuild/sunos-x64": "0.21.3", + "@esbuild/win32-arm64": "0.21.3", + "@esbuild/win32-ia32": "0.21.3", + "@esbuild/win32-x64": "0.21.3" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.21.3.tgz", + "integrity": "sha512-DMOV+eeVra0yVq3XIojfczdEQsz+RiFnpEj7lqs8Gux9mlTpN7yIbw0a4KzLspn0Uhw6UVEH3nUAidSqc/rcQg==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "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 + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/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, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/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, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "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, + "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, + "engines": { + "node": ">= 0.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 + }, + "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, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/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 + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "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, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "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, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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 + }, + "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==", + "dev": true, + "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 + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "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, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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 + }, + "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==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "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, + "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 + }, + "node_modules/finalhandler/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, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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, + "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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, + "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 + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "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, + "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, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "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, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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 + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.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, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "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 + }, + "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==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "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/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==" + }, + "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 + }, + "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==", + "dev": true, + "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, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "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, + "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, + "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.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "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, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "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, + "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 + }, + "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, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "peer": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "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" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/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" + } + ], + "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 + }, + "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 + }, + "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, + "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, + "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 + }, + "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, + "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": "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, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", + "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.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, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "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" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "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, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "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, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "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, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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 + }, + "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, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.22", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.22.tgz", + "integrity": "sha512-SqLLa/Oe5rZUagTR9z+Zd6izyatHglbmbvVofo1KzuVB54YHleWzeHNLoR7FOICGOeQSqeLh1cordb3MzhGcEw==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.2", + "@ljharb/through": "^2.3.13", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.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, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?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, + "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 + }, + "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, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-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==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "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, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "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, + "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, + "peer": true + }, + "node_modules/is-regexp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", + "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-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 + }, + "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, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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 + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.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 + }, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.1.0.tgz", + "integrity": "sha512-prmJlC1dbLhti4nE4XAPDWmfJesYO15sjGXVp7Cs7Ym5I9Xtwa/hUHxxJXjnpfLO72+ySttA0Ztf8g/RiVnUKw==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "jasmine-core": "~5.1.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", + "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", + "dev": true + }, + "node_modules/jasmine/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jasmine/node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jasmine/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.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, + "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, + "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.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "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 + }, + "node_modules/jsdom": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", + "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "dev": true, + "peer": true, + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.7", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.16.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "dev": true, + "peer": true, + "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/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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 + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "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 + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "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" + ] + }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/karma": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", + "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", + "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "dev": true + }, + "node_modules/karma-jsdom-launcher": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/karma-jsdom-launcher/-/karma-jsdom-launcher-17.0.0.tgz", + "integrity": "sha512-imnZAd77BPrWTuk+JPtl8VgaoUv4a05j85VIn9Z7iXWqYtG+fF8YiR1ftGZIno2U/peyL9XNogiXWTz49qffOg==", + "dev": true, + "peerDependencies": { + "jsdom": ">=17 <=24", + "karma": ">=2 <=6" + } + }, + "node_modules/karma-junit-reporter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-2.0.1.tgz", + "integrity": "sha512-VtcGfE0JE4OE1wn0LK8xxDKaTP7slN8DO3I+4xg6gAi1IoAHAXOJ1V9G/y45Xg6sxdxPOR3THCFtDlAfBo9Afw==", + "dev": true, + "dependencies": { + "path-is-absolute": "^1.0.0", + "xmlbuilder": "12.0.0" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-sonarqube-reporter": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-sonarqube-reporter/-/karma-sonarqube-reporter-1.4.0.tgz", + "integrity": "sha512-Dywucb6ZZMrH+aDFMgSh+b3tKQl1nc+ldVgEyRB9jfP1eraChk/ahO3eFCMPX4K2iqxerQ6l2VazabU3IXz2Sg==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.2", + "@types/karma": "*", + "@types/mkdirp": "^1.0.0", + "clone-regexp": "^2.0.0", + "glob": "^7.1.2", + "js2xmlparser": "^4.0.0", + "mkdirp": "^1.0.0", + "winston": "^3.0.0" + } + }, + "node_modules/karma-sonarqube-reporter/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma-viewport": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/karma-viewport/-/karma-viewport-1.0.9.tgz", + "integrity": "sha512-E1xVe66vBQtI66TGOtZMzV5nf6BW5tW4TQVUqPK+oakVLdsG/ZUG688tGK0lL1q0t7nfQD1dwLD8Z9Guu/RVdg==", + "dependencies": { + "@types/karma": "^6.3.3", + "jsonschema": "^1.4.0" + } + }, + "node_modules/karma-webpack": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.1.tgz", + "integrity": "sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^9.0.3", + "webpack-merge": "^4.1.5" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/karma-webpack/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/karma-webpack/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma-webpack/node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/karma/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/karma/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 + }, + "node_modules/karma/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "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": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "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, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/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, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "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, + "optional": true, + "engines": { + "node": ">=0.10.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, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": 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 + }, + "node_modules/lmdb": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.8.tgz", + "integrity": "sha512-9rp8JT4jPhCRJUL7vRARa2N06OLSYzLwQsEkhC6Qu5XbcLyM/XBLMzDlgS/K7l7c5CdURLdDk9uE+hPFIogHTQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "msgpackr": "^1.9.9", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.1.1", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.8", + "@lmdb/lmdb-darwin-x64": "3.0.8", + "@lmdb/lmdb-linux-arm": "3.0.8", + "@lmdb/lmdb-linux-arm64": "3.0.8", + "@lmdb/lmdb-linux-x64": "3.0.8", + "@lmdb/lmdb-win32-x64": "3.0.8" + } + }, + "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, + "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, + "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, + "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 + }, + "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 + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/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, + "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/log-symbols/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/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 + }, + "node_modules/log-symbols/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dev": true, + "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/logform/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, + "engines": { + "node": ">=0.1.90" + } + }, + "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==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "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/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, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.9.2.tgz", + "integrity": "sha512-f16coDZlTG1jskq3mxarwB+fGRrd0uXWt+o1WIhRfOwbXQZqUDsTVxQBFK9JjRQHblg8eAG2JSbprDXKjc7ijQ==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.1.2", + "sonic-forest": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "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 + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "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, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.6.tgz", + "integrity": "sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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, + "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, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "dev": true, + "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 + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "dev": true, + "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, + "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, + "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, + "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, + "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 + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "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, + "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 + }, + "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, + "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, + "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 + }, + "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, + "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, + "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 + }, + "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, + "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, + "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 + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "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==" + }, + "node_modules/msgpackr": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.2.tgz", + "integrity": "sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA==", + "dev": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", + "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } + }, + "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "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, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "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, + "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" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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, + "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, + "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, + "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 + }, + "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, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-napi/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, + "optional": true + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true + }, + "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, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", + "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "dev": true, + "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": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "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.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "dev": true, + "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-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/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, + "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, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", + "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "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==", + "dev": true, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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, + "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, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", + "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "dev": true, + "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/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, + "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.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.1.tgz", + "integrity": "sha512-Udm1f0l2nXb3wxDpKjfohwgdFUSV50UVwzEIpDXVsbDMXVIEF81a/i0UhuQbhrPMMmdiq3+YMFLFIRVLs3hxQw==", + "dev": true, + "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": "17.0.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.0.1.tgz", + "integrity": "sha512-fLu9MTdZTlJAHUek/VLklE6EpIiP3VZpTiuN7OOMCt2Sd67NCpSEetMaxHHEZiZxllp8ZLsUpvbEszqTFEc+wA==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^2.0.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-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true, + "peer": true + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "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 + }, + "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, + "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, + "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, + "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, + "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, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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, + "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, + "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 + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", + "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==", + "dev": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "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/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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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 + }, + "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, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.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, + "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, + "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, + "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, + "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, + "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, + "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 + }, + "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, + "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.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.5.0.tgz", + "integrity": "sha512-iBaLWI56PFP81cfBSomWTmhOo9W2/yhIOL+Tk8O1vBCpK39cM0tGxB+wgYjG31qq4ohGvysfXSdnj8h7g4rZxA==", + "dev": true, + "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, + "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, + "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, + "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, + "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, + "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, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "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, + "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 + }, + "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, + "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, + "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, + "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, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "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 + }, + "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, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.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 + }, + "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 + }, + "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, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "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, + "engines": { + "node": ">= 0.10" + } + }, + "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, + "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, + "peer": true + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "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, + "peer": true + }, + "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==", + "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" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "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, + "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, + "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/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, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true + }, + "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 + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "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 + }, + "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, + "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 + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "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==", + "dev": true, + "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, + "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 + }, + "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, + "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-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-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, + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "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==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "peer": true + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "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" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true, + "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 + }, + "node_modules/sass": { + "version": "1.77.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz", + "integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==", + "dev": true, + "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.2.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz", + "integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==", + "dev": true, + "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/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true, + "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, + "peer": true, + "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, + "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 + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "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/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, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/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 + }, + "node_modules/send/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, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/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==", + "dev": true + }, + "node_modules/send/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, + "engines": { + "node": ">= 0.8" + } + }, + "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, + "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, + "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, + "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, + "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, + "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 + }, + "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 + }, + "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 + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "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, + "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, + "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, + "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, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/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, + "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-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, + "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 + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "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.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.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, + "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, + "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, + "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.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonic-forest": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sonic-forest/-/sonic-forest-1.0.3.tgz", + "integrity": "sha512-dtwajos6IWMEWXdEbW1IkEkyL2gztCAgDplRIX+OT5aRKnEd5e7r7YCxRgXZdhRP1FBdOBf8axeTPhzDv8T4wQ==", + "dev": true, + "dependencies": { + "tree-dump": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "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, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "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, + "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, + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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 + }, + "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, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "dev": true + }, + "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, + "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, + "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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "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, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.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, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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, + "peer": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "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, + "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, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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 + }, + "node_modules/terser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", + "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", + "dev": true, + "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, + "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, + "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, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "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 + }, + "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, + "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/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-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 + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "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 + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/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==", + "dev": true, + "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==", + "dev": true, + "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, + "engines": { + "node": ">=0.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, + "peer": true, + "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, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-dump": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.1.tgz", + "integrity": "sha512-WCkcRBVPSlHHq1dc/px9iOfqklvzCbdRwvlNfxGZsrHqf6aZttfPrd7DJTt6oR10dwUfpFFQeVTkPbBIZxX/YA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/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, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/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, + "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-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, + "engines": { + "node": ">=10" + }, + "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, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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 + }, + "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==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "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" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.0.tgz", + "integrity": "sha512-nT8jjv/fE9Et1ilR6QoW8ingRTY2Pp4l2RUrdzV5Yz35RJDrtPc1DXvuNqcpsJSGIRHFdt3YKKktTzJA6r0fTA==", + "dev": true, + "engines": { + "node": ">=18.17" + } + }, + "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==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "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, + "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.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "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, + "engines": { + "node": ">=4" + } + }, + "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, + "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, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "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, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "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" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "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, + "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, + "peer": true, + "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 + }, + "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, + "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, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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, + "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, + "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, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.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.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "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, + "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, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true + }, + "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, + "peer": true, + "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, + "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-dev-middleware": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz", + "integrity": "sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "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.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/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, + "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/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/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, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "dev": true, + "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/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, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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, + "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, + "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, + "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, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "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 + }, + "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 + }, + "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, + "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/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, + "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, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "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, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "peer": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "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 + }, + "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, + "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.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "dev": true, + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/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, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "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, + "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, + "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 + }, + "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, + "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, + "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 + }, + "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 + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "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/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-12.0.0.tgz", + "integrity": "sha512-lMo8DJ8u6JRWp0/Y4XLa/atVDr75H9litKlb2E5j3V3MesoL50EBgZDWoLT3F/LztVnG67GjPXLZpqcky/UMnQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "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, + "peer": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "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==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "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==" + } + } +} diff --git a/marketplace-ui/package.json b/marketplace-ui/package.json new file mode 100644 index 000000000..f742e45fb --- /dev/null +++ b/marketplace-ui/package.json @@ -0,0 +1,55 @@ +{ + "name": "marketplace-ui", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^18.0.0", + "@angular/common": "^18.0.0", + "@angular/compiler": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "@angular/platform-browser": "^18.0.0", + "@angular/platform-browser-dynamic": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/router": "^18.0.0", + "@fortawesome/fontawesome-free": "^6.5.2", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "karma-viewport": "^1.0.9", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "yaml": "^2.4.2", + "zone.js": "~0.14.3" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.0.0", + "@angular/cli": "^18.0.0", + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@types/bootstrap": "^5.2.10", + "@types/jasmine": "~5.1.0", + "@types/node": "^18.18.0", + "jasmine": "^5.1.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "karma-jsdom-launcher": "^17.0.0", + "karma-junit-reporter": "^2.0.1", + "karma-sonarqube-reporter": "^1.4.0", + "karma-webpack": "^5.0.1", + "prettier": "^3.2.5", + "typescript": "~5.4.2" + } +} diff --git a/marketplace-ui/sonar-project.properties b/marketplace-ui/sonar-project.properties new file mode 100644 index 000000000..e2453c704 --- /dev/null +++ b/marketplace-ui/sonar-project.properties @@ -0,0 +1,5 @@ +sonar.sources=src +sonar.tests=src +sonar.exclusions=**/node_modules/**, src/assets/**, **/*.html, **/*.scss, src/app/shared/mocks/**, **/*.constant.ts, **/*.enum.ts, **/*.routes.ts, **/*.model.ts, **/*.config.ts, src/environments/** +sonar.test.inclusions=**/*.spec.ts +sonar.typescript.lcov.reportPaths=coverage/lcov.info \ No newline at end of file diff --git a/marketplace-ui/src/app/app.component.html b/marketplace-ui/src/app/app.component.html new file mode 100644 index 000000000..347c01580 --- /dev/null +++ b/marketplace-ui/src/app/app.component.html @@ -0,0 +1,21 @@ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +@if (loadingService.isLoading()) { +
+
+
+} diff --git a/marketplace-ui/src/app/app.component.scss b/marketplace-ui/src/app/app.component.scss new file mode 100644 index 000000000..bc547d095 --- /dev/null +++ b/marketplace-ui/src/app/app.component.scss @@ -0,0 +1,44 @@ +html, +body { + margin: 0; + padding: 0; + font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; +} + +header { + border-bottom: 0.5px solid var(--header-border-color); +} + +footer { + height: 409px; + padding: 0 0 56px 0; + border-top: 1px solid var(--ivy-secondary-border-color); + margin-top: 64.14px; +} + +@media all and (max-width: 992px) { + .w-md-100 { + width: -webkit-fill-available; + } + .m-t-5 { + margin-top: 5%; + } +} + +.spinner-container { + position: fixed; + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.32); + z-index: 2000; +} + +.spinner-border { + width: 4rem; + height: 4rem; +} diff --git a/marketplace-ui/src/app/app.component.spec.ts b/marketplace-ui/src/app/app.component.spec.ts new file mode 100644 index 000000000..f1a06df2b --- /dev/null +++ b/marketplace-ui/src/app/app.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { AppComponent } from './app.component'; +import { By } from '@angular/platform-browser'; + +describe('AppComponent', () => { + let component: AppComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create the app', () => { + expect(component).toBeTruthy(); + }); + + it('default active nav should be Market', () => { + const activeNav = fixture.debugElement.query( + By.css('a.nav-link.text-primary.fw-bold.active') + ).nativeElement; + expect(activeNav.innerHTML).toContain('common.nav.market'); + }); +}); diff --git a/marketplace-ui/src/app/app.component.ts b/marketplace-ui/src/app/app.component.ts new file mode 100644 index 000000000..6806884de --- /dev/null +++ b/marketplace-ui/src/app/app.component.ts @@ -0,0 +1,16 @@ +import { Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { FooterComponent } from './shared/components/footer/footer.component'; +import { HeaderComponent } from './shared/components/header/header.component'; +import { LoadingService } from './core/services/loading/loading.service'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet, HeaderComponent, FooterComponent], + templateUrl: './app.component.html', + styleUrl: './app.component.scss' +}) +export class AppComponent { + loadingService = inject(LoadingService); +} diff --git a/marketplace-ui/src/app/app.config.ts b/marketplace-ui/src/app/app.config.ts new file mode 100644 index 000000000..14db020f5 --- /dev/null +++ b/marketplace-ui/src/app/app.config.ts @@ -0,0 +1,33 @@ +import { + HttpClient, + provideHttpClient, + withFetch, + withInterceptors +} from '@angular/common/http'; +import { + ApplicationConfig, + importProvidersFrom, + provideExperimentalZonelessChangeDetection +} from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { routes } from './app.routes'; +import { httpLoaderFactory } from './core/configs/translate.config'; +import { apiInterceptor } from './core/interceptors/api.interceptor'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideExperimentalZonelessChangeDetection(), + provideRouter(routes), + provideHttpClient(withFetch(), withInterceptors([apiInterceptor])), + importProvidersFrom( + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient] + } + }) + ) + ] +}; diff --git a/marketplace-ui/src/app/app.routes.ts b/marketplace-ui/src/app/app.routes.ts new file mode 100644 index 000000000..4b9644f26 --- /dev/null +++ b/marketplace-ui/src/app/app.routes.ts @@ -0,0 +1,14 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadChildren: () => + import('./modules/home/home.routes').then((m) => m.routes), + }, + { + path: ':id', + loadChildren: () => + import('./modules/product/product.routes').then((m) => m.routes), + }, +]; diff --git a/marketplace-ui/src/app/core/configs/translate.config.ts b/marketplace-ui/src/app/core/configs/translate.config.ts new file mode 100644 index 000000000..9c6ad62c0 --- /dev/null +++ b/marketplace-ui/src/app/core/configs/translate.config.ts @@ -0,0 +1,21 @@ +import { HttpClient } from '@angular/common/http'; +import { TranslateLoader } from '@ngx-translate/core'; +import { Observable, map } from 'rxjs'; +import { parse } from 'yaml'; + +class TranslateYamlHttpLoader implements TranslateLoader { + constructor( + private readonly http: HttpClient, + public path = '/assets/i18n/' + ) {} + + public getTranslation(lang: string): Observable { + return this.http + .get(`${this.path}${lang}.yaml`, { responseType: 'text' }) + .pipe(map(data => parse(data))); + } +} + +export function httpLoaderFactory(httpClient: HttpClient) { + return new TranslateYamlHttpLoader(httpClient); +} diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts new file mode 100644 index 000000000..936e55e82 --- /dev/null +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -0,0 +1,46 @@ +import { HttpHeaders, HttpContextToken, HttpInterceptorFn } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; +import { LoadingService } from '../services/loading/loading.service'; +import { inject } from '@angular/core'; +import { finalize } from 'rxjs'; + +export const REQUEST_BY = "X-Requested-By"; +export const IVY = "ivy"; + +/** This is option for exclude loading api + * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(SkipLoading, true) }) + */ +export const SkipLoading = new HttpContextToken(() => false); + +export const apiInterceptor: HttpInterceptorFn = (req, next) => { + const loadingService = inject(LoadingService); + + if (req.url.includes('i18n')) { + return next(req); + } + let requestURL = req.url; + const apiURL = environment.apiUrl; + if (!requestURL.startsWith(apiURL)) { + requestURL = `${apiURL}/${req.url}`; + } + const cloneReq = req.clone({ url: requestURL, headers: addIvyHeaders(req.headers) }); + + if (req.context.get(SkipLoading)) { + return next(cloneReq); + } + + loadingService.show(); + + return next(cloneReq).pipe( + finalize(() => { + loadingService.hide(); + }) + ); +}; + +function addIvyHeaders(headers: HttpHeaders): HttpHeaders { + if (headers.has(REQUEST_BY)) { + return headers; + } + return headers.append(REQUEST_BY, IVY); +} diff --git a/marketplace-ui/src/app/core/services/language/language.service.spec.ts b/marketplace-ui/src/app/core/services/language/language.service.spec.ts new file mode 100644 index 000000000..14a2b853e --- /dev/null +++ b/marketplace-ui/src/app/core/services/language/language.service.spec.ts @@ -0,0 +1,30 @@ +import { TestBed } from '@angular/core/testing'; +import { LanguageService } from './language.service'; +import { Language } from '../../../shared/enums/language.enum'; + +describe('LanguageService', () => { + let service: LanguageService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [LanguageService], + }); + service = TestBed.inject(LanguageService); + }); + + it('should be created', () => { + document.defaultView?.localStorage.clear(); + expect(service).toBeTruthy(); + }); + + it('should get default language en', () => { + document.defaultView?.localStorage.clear(); + expect(service.getSelectedLanguage()).toEqual(Language.EN); + }); + + it('should change to language de-DE', ()=> { + service.loadLanguage("de"); + expect(service.getSelectedLanguage()).toEqual(Language.DE); + }); +}); diff --git a/marketplace-ui/src/app/core/services/language/language.service.ts b/marketplace-ui/src/app/core/services/language/language.service.ts new file mode 100644 index 000000000..85a2732a3 --- /dev/null +++ b/marketplace-ui/src/app/core/services/language/language.service.ts @@ -0,0 +1,28 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; +import { Language } from '../../../shared/enums/language.enum'; + +const DATA_LANGUAGE = 'data-language'; + +@Injectable({ providedIn: 'root' }) +export class LanguageService { + constructor(@Inject(DOCUMENT) private readonly document: Document) { + const localStorage = this.document.defaultView?.localStorage; + if (localStorage) { + this.loadDefaultLanguage(localStorage); + } + } + + loadDefaultLanguage(localStorage: Storage) { + const language = localStorage.getItem(DATA_LANGUAGE); + this.loadLanguage(language ?? Language.EN); + } + + loadLanguage(language: string): void { + localStorage.setItem(DATA_LANGUAGE, language); + } + + getSelectedLanguage(): Language { + return localStorage.getItem(DATA_LANGUAGE) as Language ?? Language.EN; + } +} diff --git a/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts b/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts new file mode 100644 index 000000000..d6640c9e5 --- /dev/null +++ b/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts @@ -0,0 +1,26 @@ +import { TestBed } from '@angular/core/testing'; + +import { LoadingService } from './loading.service'; + +describe('LoadingService', () => { + let service: LoadingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LoadingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('show should update isLoading to true', () => { + service.show(); + expect(service.isLoading()).toBeTrue(); + }) + + it('hide should update isLoading to false', () => { + service.hide(); + expect(service.isLoading()).toBeFalse(); + }) +}); diff --git a/marketplace-ui/src/app/core/services/loading/loading.service.ts b/marketplace-ui/src/app/core/services/loading/loading.service.ts new file mode 100644 index 000000000..0536aa06c --- /dev/null +++ b/marketplace-ui/src/app/core/services/loading/loading.service.ts @@ -0,0 +1,17 @@ +import { Injectable, computed, signal } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LoadingService { + private readonly isShow = signal(false); + isLoading = computed(() => this.isShow()); + + show() { + this.isShow.set(true); + } + + hide() { + this.isShow.set(false); + } +} diff --git a/marketplace-ui/src/app/core/services/theme/theme.service.spec.ts b/marketplace-ui/src/app/core/services/theme/theme.service.spec.ts new file mode 100644 index 000000000..d6d7a87f1 --- /dev/null +++ b/marketplace-ui/src/app/core/services/theme/theme.service.spec.ts @@ -0,0 +1,42 @@ +import { TestBed } from '@angular/core/testing'; +import { Theme } from '../../../shared/enums/theme.enum'; +import { ThemeService } from './theme.service'; + +describe('ThemeService', () => { + let service: ThemeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ThemeService], + }); + service = TestBed.inject(ThemeService); + }); + + it('should be created', () => { + document.defaultView?.localStorage.clear(); + expect(service).toBeTruthy(); + }); + + it('setTheme light', () => { + service.setTheme(Theme.LIGHT); + expect(service.theme()).toEqual(Theme.LIGHT); + }); + + it('setTheme dark', () => { + service.setTheme(Theme.DARK); + expect(service.theme()).toEqual(Theme.DARK); + }); + + it('changeTheme to light', () => { + service.setTheme(Theme.DARK); + service.changeTheme(); + expect(service.theme()).toEqual(Theme.LIGHT); + }); + + it('changeTheme to dark', () => { + service.setTheme(Theme.LIGHT); + service.changeTheme(); + expect(service.theme()).toEqual(Theme.DARK); + }); +}); diff --git a/marketplace-ui/src/app/core/services/theme/theme.service.ts b/marketplace-ui/src/app/core/services/theme/theme.service.ts new file mode 100644 index 000000000..acc332e75 --- /dev/null +++ b/marketplace-ui/src/app/core/services/theme/theme.service.ts @@ -0,0 +1,46 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, WritableSignal, signal } from '@angular/core'; +import { Theme } from '../../../shared/enums/theme.enum'; + +const DATA_THEME = 'data-bs-theme'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + isDarkMode: WritableSignal = signal(false); + theme: WritableSignal = signal(Theme.DARK); + + constructor(@Inject(DOCUMENT) private readonly document: Document) { + const localStorage = this.document.defaultView?.localStorage; + if (localStorage) { + this.loadDefaultTheme(localStorage); + } + } + + loadDefaultTheme(localStorage: Storage) { + const theme = localStorage.getItem(DATA_THEME) as Theme; + if (theme) { + this.setTheme(theme); + } else { + this.setTheme(Theme.LIGHT); + } + } + + setTheme(theme: Theme) { + this.theme.set(theme); + localStorage.setItem(DATA_THEME, theme); + const html = this.document.querySelector('html'); + if (html) { + html.setAttribute(DATA_THEME, theme); + } + this.isDarkMode.set(this.theme() === Theme.DARK); + } + + changeTheme() { + if (this.theme() === Theme.DARK) { + this.theme.set(Theme.LIGHT); + } else { + this.theme.set(Theme.DARK); + } + this.setTheme(this.theme()); + } +} diff --git a/marketplace-ui/src/app/modules/home/home.component.html b/marketplace-ui/src/app/modules/home/home.component.html new file mode 100644 index 000000000..906929081 --- /dev/null +++ b/marketplace-ui/src/app/modules/home/home.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/marketplace-ui/src/app/modules/home/home.component.scss b/marketplace-ui/src/app/modules/home/home.component.scss new file mode 100644 index 000000000..36e5ce5a9 --- /dev/null +++ b/marketplace-ui/src/app/modules/home/home.component.scss @@ -0,0 +1,7 @@ +.home { + display: flex; + justify-content: center; + align-items: center; + width: 500px; + height: 500px; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/home/home.component.spec.ts b/marketplace-ui/src/app/modules/home/home.component.spec.ts new file mode 100644 index 000000000..453fa72b3 --- /dev/null +++ b/marketplace-ui/src/app/modules/home/home.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomeComponent, TranslateModule.forRoot()], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + TranslateService + ] + }).compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/marketplace-ui/src/app/modules/home/home.component.ts b/marketplace-ui/src/app/modules/home/home.component.ts new file mode 100644 index 000000000..915e2b378 --- /dev/null +++ b/marketplace-ui/src/app/modules/home/home.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { ProductComponent } from '../product/product.component'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [ProductComponent], + templateUrl: './home.component.html', + styleUrl: './home.component.scss', +}) +export class HomeComponent {} diff --git a/marketplace-ui/src/app/modules/home/home.routes.ts b/marketplace-ui/src/app/modules/home/home.routes.ts new file mode 100644 index 000000000..231b8bef1 --- /dev/null +++ b/marketplace-ui/src/app/modules/home/home.routes.ts @@ -0,0 +1,9 @@ +import { Route } from '@angular/router'; + +export const routes: Route[] = [ + { + path: '', + loadComponent: () => + import('./home.component').then((m) => m.HomeComponent), + }, +]; diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html new file mode 100644 index 000000000..dc91c8667 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html @@ -0,0 +1,28 @@ +
+
+ +
+ {{ 'common.filter.value.' + product.type | translate }} +
+
+
+
+ {{ + product.names | multilingualism: languageService.getSelectedLanguage() + }} +
+

+ {{ + product.shortDescriptions + | multilingualism: languageService.getSelectedLanguage() + }} +

+
+
diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss b/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss new file mode 100644 index 000000000..46683984f --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss @@ -0,0 +1,26 @@ +img { + width: 70px; + height: 70px; +} + +.card { + background-color: var(--ivy-secondary-bg); + border: 1px solid var(--ivy-border-color); +} + +.card__title { + font-weight: 600; + font-size: 20px; +} + +.card__tag { + border-radius: 5px; + background-color: var(--ivy-primary-bg); + font-size: 14px; +} + +.card__description { + font-weight: 400; + font-size: 16px; + color: var(--ivy-secondary-text-dark-color); +} diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts new file mode 100644 index 000000000..79fe77af4 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { + MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS, + MOCK_PRODUCTS +} from '../../../shared/mocks/mock-data'; +import { ProductCardComponent } from './product-card.component'; +import { Product } from '../../../shared/models/product.model'; +import { Language } from '../../../shared/enums/language.enum'; + +const products = MOCK_PRODUCTS._embedded.products as Product[]; +const noDeNameAndNoLogoUrlProducts = + MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS._embedded.products as Product[]; + +describe('ProductCardComponent', () => { + let component: ProductCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductCardComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(ProductCardComponent); + component = fixture.componentInstance; + component.product = products[0]; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should load default value when german value is empty', () => { + component.product = noDeNameAndNoLogoUrlProducts[0]; + component.languageService.loadLanguage(Language.DE); + fixture.detectChanges(); + expect( + document + .getElementsByClassName('card__title') + .item(0) + ?.textContent?.trim() + ).toEqual('Amazon Comprehend'); + expect( + document + .getElementsByClassName('card__description') + .item(0) + ?.textContent?.trim() + ).toEqual( + 'Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.' + ); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts new file mode 100644 index 000000000..307436298 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts @@ -0,0 +1,22 @@ +import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { Component, Input, inject } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { LanguageService } from '../../../core/services/language/language.service'; +import { ThemeService } from '../../../core/services/theme/theme.service'; +import { Product } from '../../../shared/models/product.model'; +import { ProductLogoPipe } from '../../../shared/pipes/logo.pipe'; +import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe'; + +@Component({ + selector: 'app-product-card', + standalone: true, + imports: [CommonModule, ProductLogoPipe, MultilingualismPipe, TranslateModule, NgOptimizedImage], + templateUrl: './product-card.component.html', + styleUrl: './product-card.component.scss' +}) +export class ProductCardComponent { + themeService = inject(ThemeService); + languageService = inject(LanguageService); + + @Input() product!: Product; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html new file mode 100644 index 000000000..dfbb4262d --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html @@ -0,0 +1,85 @@ +
+ + + @if (isDropDownDisplayed()) { +
+ + } +
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss new file mode 100644 index 000000000..e5d1aa35a --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss @@ -0,0 +1,178 @@ +::ng-deep .custom-tooltip { + --bs-tooltip-bg: var(--bs-body-bg); + --bs-tooltip-color: black; + --bs-tooltip-font-size: 12px; + --bs-tooltip-opacity: inherit !important; + background-color: var(--bs-body-bg); + min-width: 371px; + padding: 10px; + gap: 0px; + border-radius: 5px; + justify-content: space-between; + box-shadow: 0px 4px 30px 0px #0000001a; + border: 0.5px solid var(--ivy-secondary-border-color); + top: 0.8rem !important; + + .tooltip-arrow { + top: -0.8rem !important; + width: 0; + height: 0; + border-left: 13px solid transparent; + border-right: 13px solid transparent; + border-bottom: 13px solid var(--ivy-secondary-border-color); + &::before { + width: 0; + height: 0; + border-left: 12px solid transparent !important; + border-right: 12px solid transparent !important; + border-bottom: 12px solid var(--bs-body-bg) !important; + top: 0.15rem !important; + left: -0.75rem; + } + } + + .tooltip-inner { + padding: 0px; + min-width: 351px; + height: 30px; + gap: 0px; + font-weight: 400; + line-height: 15px; + letter-spacing: 0.01em; + text-align: left; + .ivy__link { + color: var(--ivy-link-corlor); + } + } +} + +.fs-md { + font-size: 12px; +} + +.fs-small { + font-size: 10px; +} + +.product-detail__versions-action { + .form-label { + font-weight: 400; + line-height: 14.52px; + text-align: left; + } + .border__dropdown { + border: 0.5px solid var(--ivy-secondary-border-color); + } + + .btn { + padding: 12px 32px; + gap: 10px; + font-weight: 500; + size: 16px; + line-height: 120%; + border-radius: 10px; + border: none; + } + + .btn__install { + margin-right: 10px; + border: 0px; + } + + .btn__download { + border: 0.5px solid #ebebeb; + } + + .primary-color { + color: var(--ivy-primary-bg); + } + + .dropdown-menu { + padding: 10px; + border-radius: 5px; + gap: 15px; + box-shadow: 0px 4px 30px 0px rgba(0, 0, 0, 0.1); + + &.maven-artifact-version__action { + left: 2rem; + min-width: 400px; + min-height: 204px; + top: 1rem; + @media (max-width: 500px) { + left: auto; + right: auto; + min-width: 80vw; + } + + .form-group { + gap: 7px; + + .form-select { + height: 32px; + padding: 9px 10px; + gap: 10px; + border-radius: 5px; + line-height: 12px; + text-align: start !important; + background-color: var(--ivy-text-normal-color); + option { + height: 44px; + padding: 15px; + gap: 10px; + font-size: 14px; + font-weight: 400; + line-height: 16.8px; + } + } + + .artifacts-selector__dropdown { + --bs-form-select-bg-img: none !important; + } + + .indicator-arrow__up { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 11 8 5 14 11'/%3e%3c/svg%3e") !important; + } + } + } + + .dev-versions__toggle { + font-size: 10px; + } + + .form-download__btn { + padding: 5px 16px; + gap: 6px; + border-radius: 5px; + font-size: 14px; + font-weight: 400; + line-height: 16.8px; + } + } + + .up-arrow { + transform: rotate(45deg); + top: 2.45rem; + z-index: 1001; + width: 20px; + height: 20px; + border-right-color: transparent; + border-bottom-color: transparent; + background-color: var(--bs-body-bg); + + &.up-arrow__action { + left: 11rem; + + @media (max-width: 992px) { + left: 11.5rem; + } + + @media (max-width: 767px) { + left: 18.5rem; + } + + @media (max-width: 575px) { + left: 54vw; + } + } + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts new file mode 100644 index 000000000..a926632a5 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts @@ -0,0 +1,174 @@ +import { + ComponentFixture, + TestBed, + fakeAsync, + tick +} from '@angular/core/testing'; +import { of } from 'rxjs'; +import { ProductDetailVersionActionComponent } from './product-detail-version-action.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ProductService } from '../../product.service'; +import { provideHttpClient } from '@angular/common/http'; +import { Artifact } from '../../../../shared/models/vesion-artifact.model'; +describe('ProductVersionActionComponent', () => { + let component: ProductDetailVersionActionComponent; + let fixture: ComponentFixture; + let productServiceMock: any; + + beforeEach(() => { + productServiceMock = jasmine.createSpyObj('ProductService', [ + 'sendRequestToProductDetailVersionAPI' + ]); + + TestBed.configureTestingModule({ + imports: [ProductDetailVersionActionComponent, TranslateModule.forRoot()], + providers: [ + TranslateService, + provideHttpClient(), + { provide: ProductService, useValue: productServiceMock } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(ProductDetailVersionActionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('first artifact should be chosen when select corresponding version', () => { + component.onSelectVersion(); + expect(component.artifacts().length).toBe(0); + + const selectedVersion = 'Version 10.0.2'; + const artifact = { + name: 'Example Artifact', + downloadUrl: 'https://example.com/download', + isProductArtifact: true + } as Artifact; + component.versions.set([selectedVersion]); + component.versionMap.set(selectedVersion, [artifact]); + component.selectedVersion = selectedVersion; + component.onSelectVersion(); + + expect(component.artifacts().length).toBe(1); + expect(component.selectedArtifact).toEqual('https://example.com/download'); + }); + + it('all of state should be reset before call rest api', () => { + const selectedVersion = 'Version 10.0.2'; + const artifact = { + name: 'Example Artifact', + downloadUrl: 'https://example.com/download', + isProductArtifact: true + } as Artifact; + component.selectedVersion = selectedVersion; + component.selectedArtifact = artifact.downloadUrl; + component.versions().push(selectedVersion); + component.artifacts().push(artifact); + + expect(component.versions().length).toBe(1); + expect(component.artifacts().length).toBe(1); + expect(component.selectedVersion).toBe(selectedVersion); + expect(component.selectedArtifact).toBe('https://example.com/download'); + component.sanitizeDataBeforFetching(); + expect(component.versions().length).toBe(0); + expect(component.artifacts().length).toBe(0); + expect(component.selectedVersion).toEqual(''); + expect(component.selectedArtifact).toEqual(''); + }); + + it('should call sendRequestToProductDetailVersionAPI and update versions and versionMap', () => { + const { mockArtifct1, mockArtifct2 } = mockApiWithExpectedResponse(); + + component.getVersionWithArtifact(); + + expect( + productServiceMock.sendRequestToProductDetailVersionAPI + ).toHaveBeenCalledWith( + component.productId, + component.isDevVersionsDisplayed(), + component.designerVersion + ); + + expect(component.versions()).toEqual(['Version 1.0', 'Version 2.0']); + expect(component.versionMap.get('Version 1.0')).toEqual([mockArtifct1]); + expect(component.versionMap.get('Version 2.0')).toEqual([mockArtifct2]); + expect(component.selectedVersion).toBe('Version 1.0'); + }); + + it('should open the artifact download URL in a new window', () => { + spyOn(window, 'open'); + component.selectedArtifact = 'https://example.com/download'; + component.downloadArifact(); + expect(window.open).toHaveBeenCalledWith( + 'https://example.com/download', + '_blank' + ); + }); + it('should call getVersionWithArtifact and toggle isDropDownDisplayed', () => { + expect(component.isDropDownDisplayed()).toBeFalse(); + + mockApiWithExpectedResponse(); + component.onShowVersionAndArtifact(); + expect(component.isDropDownDisplayed()).toBeTrue(); + }); + + it('should send Api to get DevVersion', () => { + expect(component.isDevVersionsDisplayed()).toBeFalse(); + mockApiWithExpectedResponse(); + const event = new Event('click'); + spyOn(event, 'preventDefault'); + component.onShowDevVersion(event); + expect(event.preventDefault).toHaveBeenCalled(); + expect(component.isDevVersionsDisplayed()).toBeTrue(); + }); + + function mockApiWithExpectedResponse() { + const mockArtifct1 = { + name: 'Example Artifact1', + downloadUrl: 'https://example.com/download', + isProductArtifact: true + } as Artifact; + const mockArtifct2 = { + name: 'Example Artifact2', + downloadUrl: 'https://example.com/download', + isProductArtifact: true + } as Artifact; + const mockData = [ + { + version: '1.0', + artifactsByVersion: [mockArtifct1] + }, + { + version: '2.0', + artifactsByVersion: [mockArtifct2] + } + ]; + + productServiceMock.sendRequestToProductDetailVersionAPI.and.returnValue( + of(mockData) + ); + return { mockArtifct1, mockArtifct2 }; + } + + it('should return correct class based on isVersionsDropDownShow', () => { + component.isVersionsDropDownShow.set(true); + expect(component.getIndicatorClass()).toBe('indicator-arrow__up'); + + component.isVersionsDropDownShow.set(false); + expect(component.getIndicatorClass()).toBe(''); + }); + + it('should toggle isVersionsDropDownShow on calling onShowVersions', () => { + const initialState = component.isVersionsDropDownShow(); + + component.onShowVersions(); + expect(component.isVersionsDropDownShow()).toBe(!initialState); + + component.onShowVersions(); + expect(component.isVersionsDropDownShow()).toBe(initialState); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts new file mode 100644 index 000000000..e4a549022 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts @@ -0,0 +1,133 @@ +import { + AfterViewInit, + Component, + inject, + Input, + signal, + WritableSignal +} from '@angular/core'; +import { ThemeService } from '../../../../core/services/theme/theme.service'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ProductService } from '../../product.service'; +import { Artifact } from '../../../../shared/models/vesion-artifact.model'; +import { Tooltip } from 'bootstrap'; + +const delayTimeBeforeHideMessage = 2000; +@Component({ + selector: 'app-product-version-action', + standalone: true, + imports: [CommonModule, TranslateModule, FormsModule], + templateUrl: './product-detail-version-action.component.html', + styleUrl: './product-detail-version-action.component.scss' +}) +export class ProductDetailVersionActionComponent implements AfterViewInit { + ngAfterViewInit() { + const tooltipTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="tooltip"]') + ); + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new Tooltip(tooltipTriggerEl); + }); + } + + @Input() + productId!: string; + versions: WritableSignal = signal([]); + artifacts: WritableSignal = signal([]); + themeService = inject(ThemeService); + translateService = inject(TranslateService); + isDevVersionsDisplayed = signal(false); + isDropDownDisplayed = signal(false); + isVersionsDropDownShow = signal(false); + isDesignerEnvironment = signal(false); + isInvalidInstallationEnvironment = signal(false); + designerVersion = ''; + selectedArtifact = ''; + selectedVersion!: string; + productService = inject(ProductService); + versionMap: Map = new Map(); + + getIndicatorClass() { + if (this.isVersionsDropDownShow()) { + return 'indicator-arrow__up'; + } + return ''; + } + + onShowVersions() { + this.isVersionsDropDownShow.set(!this.isVersionsDropDownShow()); + } + + getInstallationTooltipText() { + return `Please open the + Axon Ivy Market + inside your + Axon Ivy Designer + (minimum version 9.2.0)`; + } + + onSelectVersion() { + this.artifacts.set(this.versionMap.get(this.selectedVersion) || []); + + if (this.artifacts().length !== 0) { + this.selectedArtifact = this.artifacts()[0].downloadUrl; + } + } + + onShowDevVersion(event: Event) { + event.preventDefault(); + this.isDevVersionsDisplayed.set(!this.isDevVersionsDisplayed()); + this.getVersionWithArtifact(); + } + + onShowVersionAndArtifact() { + if (!this.isDropDownDisplayed() && this.artifacts().length === 0) { + this.getVersionWithArtifact(); + } + this.isDropDownDisplayed.set(!this.isDropDownDisplayed()); + } + + getVersionWithArtifact() { + this.sanitizeDataBeforFetching(); + + this.productService + .sendRequestToProductDetailVersionAPI( + this.productId, + this.isDevVersionsDisplayed(), + this.designerVersion + ) + .subscribe(data => { + data.forEach(item => { + const version = 'Version '.concat(item.version); + this.versions.update(currentVersions => [ + ...currentVersions, + version + ]); + if (!this.versionMap.get(version)) { + this.versionMap.set(version, item.artifactsByVersion); + } + }); + if (this.versions().length !== 0) { + this.selectedVersion = this.versions()[0]; + this.onSelectVersion(); + } + }); + } + + sanitizeDataBeforFetching() { + this.versions.set([]); + this.artifacts.set([]); + this.selectedArtifact = ''; + this.selectedVersion = ''; + } + + downloadArifact() { + const newTab = window.open(this.selectedArtifact, '_blank'); + if (newTab) { + newTab.blur(); + } + window.focus(); + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html new file mode 100644 index 000000000..5c3494f19 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html @@ -0,0 +1,18 @@ +
+
+

+ {{ + product.names | multilingualism: languageService.getSelectedLanguage() + }} +

+

+ {{ + product.shortDescriptions + | multilingualism: languageService.getSelectedLanguage() + }} +

+
+ +
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts new file mode 100644 index 000000000..834b5b654 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { MOCK_PRODUCTS } from '../../../shared/mocks/mock-data'; +import { MockProductService } from '../../../shared/mocks/mock-services'; +import { ProductService } from '../product.service'; +import { ProductDetailComponent } from './product-detail.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { Product } from '../../../shared/models/product.model'; + +const products = MOCK_PRODUCTS._embedded.products as Product[]; + +describe('ProductDetailComponent', () => { + let component: ProductDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductDetailComponent, TranslateModule.forRoot()], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: { id: products[0].id } + } + } + } + ] + }) + .overrideComponent(ProductDetailComponent, { + remove: { providers: [ProductService] }, + add: { + providers: [{ provide: ProductService, useClass: MockProductService }] + } + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component.product.names.en).toEqual(products[0].names.en); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts new file mode 100644 index 000000000..1d965d7a7 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts @@ -0,0 +1,33 @@ +import { Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Product } from '../../../shared/models/product.model'; +import { ProductService } from '../product.service'; +import { LanguageService } from '../../../core/services/language/language.service'; +import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe'; +import { ProductDetailVersionActionComponent } from './product-detail-version-action/product-detail-version-action.component'; + +@Component({ + selector: 'app-product-detail', + standalone: true, + imports: [MultilingualismPipe, ProductDetailVersionActionComponent], + providers: [ProductService], + templateUrl: './product-detail.component.html', + styleUrl: './product-detail.component.scss' +}) +export class ProductDetailComponent { + product!: Product; + route = inject(ActivatedRoute); + productService = inject(ProductService); + languageService = inject(LanguageService); + productId!: string; + + constructor() { + const productId = this.route.snapshot.params['id']; + if (productId) { + this.productId = productId; + this.productService.getProductById(productId).subscribe(product => { + this.product = product; + }); + } + } +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html new file mode 100644 index 000000000..519f6db4c --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html @@ -0,0 +1,79 @@ +
+

+ {{ translateService.get('common.filter.label') | async }} +

+
+ +
+ @for (type of types; track $index) { +
+

+ {{ type.label | translate }} +

+
+ } +
+
+ +
+ + +
+

+ {{ translateService.get('common.sort.label') | async }}: +

+ +
+
+ + +
+
+ + + +
+ +
+
diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss new file mode 100644 index 000000000..df6c488d0 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss @@ -0,0 +1,64 @@ +$rowHeight: 37px; + +.filter-container { + height: $rowHeight; +} + +.filter-container__button { + height: $rowHeight; +} + +.sort-container { + height: $rowHeight; + font-weight: 400; + font-size: 14px; +} + +.filter-type { + height: $rowHeight; + width: max-content; + line-height: 16.8px; + border-radius: 10px; + padding: 10px 15px; + margin: 0 15px 0 0; + gap: 10px; + cursor: default; +} + +.sort-container__label { + line-height: 25.2px; +} + +.form-select { + height: $rowHeight; + line-height: 16.8px; + font-weight: 400; + font-size: 14px; + text-align: left; + border-radius: 10px; + padding: 10px 15px; + gap: 10px; + opacity: 80%; +} + +.input__search { + height: 43px; +} + +@media only screen and (max-width: 768px) { + .form-select { + width: 100% !important; + } +} + +@media only screen and (max-width: 996px) { + .sort-container__label { + display: none; + } +} + +@media only screen and (max-width: 1366px) { + .filter-type { + font-size: 1rem; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts new file mode 100644 index 000000000..6ff88295c --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts @@ -0,0 +1,76 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TypeOption } from '../../../shared/enums/type-option.enum'; +import { SortOption } from '../../../shared/enums/sort-option.enum'; +import { ProductFilterComponent } from './product-filter.component'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; + +declare const viewport: Viewport; + +describe('ProductFilterComponent', () => { + let component: ProductFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductFilterComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(ProductFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('onSelectedType should update selectedTypeOption correctly', () => { + const filterElement = fixture.debugElement.queryAll( + By.css('.filter-type') + )[1].nativeElement as HTMLDivElement; + + filterElement.dispatchEvent(new Event('click')); + expect(component.selectedType).toEqual(TypeOption.CONNECTORS); + }); + + it('filter type should change to selectbox in small screen', () => { + viewport.set(540); + const filterSelect = fixture.debugElement.query( + By.css('.filter-type--select') + ); + + expect(getComputedStyle(filterSelect.nativeElement).display).not.toBe( + 'none' + ); + }); + + it('sort label should not display in small screen', () => { + viewport.set(900); + const sortLabel = fixture.debugElement.query( + By.css('.sort-container__label') + ); + expect(getComputedStyle(sortLabel.nativeElement).display).toBe('none'); + }); + + it('onSortChange should update selectedSortOption correctly', () => { + const select: HTMLSelectElement = fixture.debugElement.query( + By.css('.sort-type') + ).nativeElement; + select.value = select.options[2].value; + select.dispatchEvent(new Event('change')); + fixture.detectChanges(); + expect(component.selectedSort).toEqual(SortOption.RECENT); + }); + + it('search should update searchText correctly', () => { + const searchText = 'portal'; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = searchText; + input.dispatchEvent(new Event('input')); + expect(component.searchText).toEqual(searchText); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts new file mode 100644 index 000000000..d8730c47a --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts @@ -0,0 +1,47 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Output, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ThemeService } from '../../../core/services/theme/theme.service'; +import { + FILTER_TYPES, + SORT_TYPES +} from '../../../shared/constants/common.constant'; +import { TypeOption } from '../../../shared/enums/type-option.enum'; +import { SortOption } from '../../../shared/enums/sort-option.enum'; + +@Component({ + selector: 'app-product-filter', + standalone: true, + imports: [CommonModule, FormsModule, TranslateModule], + templateUrl: './product-filter.component.html', + styleUrl: './product-filter.component.scss' +}) +export class ProductFilterComponent { + @Output() searchChange = new EventEmitter(); + @Output() filterChange = new EventEmitter(); + @Output() sortChange = new EventEmitter(); + + selectedType = TypeOption.All_TYPES; + types = FILTER_TYPES; + selectedSort: SortOption = SortOption.POPULARITY; + sorts = SORT_TYPES; + + searchText = ''; + + themeService = inject(ThemeService); + translateService = inject(TranslateService); + + onSelectType(type: TypeOption) { + this.selectedType = type; + this.filterChange.emit(type); + } + + onSearchChanged(searchString: string) { + this.searchChange.next(searchString); + } + + onSortChange() { + this.sortChange.next(this.selectedSort); + } +} diff --git a/marketplace-ui/src/app/modules/product/product.component.html b/marketplace-ui/src/app/modules/product/product.component.html new file mode 100644 index 000000000..efe03108d --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.component.html @@ -0,0 +1,45 @@ +
+
+

+ {{ translateService.get('common.branch') | async }} +

+
+

+ {{ translateService.get('common.introduction.about') | async }} +

+
+
+

+ {{ translateService.get('common.introduction.contribute') | async }} +

+

+
+
+ + + + @if (products().length > 0) { +
+ @for (product of products(); track $index) { +
+ +
+ } +
+ } @else { +
+ {{ 'common.nothingFound' | translate }} +
+ } + +
+
diff --git a/marketplace-ui/src/app/modules/product/product.component.scss b/marketplace-ui/src/app/modules/product/product.component.scss new file mode 100644 index 000000000..47ff283db --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.component.scss @@ -0,0 +1,3 @@ +.product-container { + min-height: 6em; +} diff --git a/marketplace-ui/src/app/modules/product/product.component.spec.ts b/marketplace-ui/src/app/modules/product/product.component.spec.ts new file mode 100644 index 000000000..071c54229 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.component.spec.ts @@ -0,0 +1,159 @@ +import { + ComponentFixture, + TestBed, + fakeAsync, + tick +} from '@angular/core/testing'; + +import { provideHttpClient } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +import { TypeOption } from '../../shared/enums/type-option.enum'; +import { SortOption } from '../../shared/enums/sort-option.enum'; +import { ProductComponent } from './product.component'; +import { ProductService } from './product.service'; +import { MockProductService } from '../../shared/mocks/mock-services'; + +const router = { + navigate: jasmine.createSpy('navigate') +}; + +describe('ProductComponent', () => { + let component: ProductComponent; + let fixture: ComponentFixture; + let mockIntersectionObserver: any; + + beforeAll(() => { + mockIntersectionObserver = jasmine.createSpyObj('IntersectionObserver', ['observe', 'unobserve', 'disconnect']); + mockIntersectionObserver.observe.and.callFake(() => { }); + mockIntersectionObserver.unobserve.and.callFake(() => { }); + mockIntersectionObserver.disconnect.and.callFake(() => { }); + + (window as any).IntersectionObserver = function (callback: IntersectionObserverCallback) { + mockIntersectionObserver.callback = callback; + return mockIntersectionObserver; + }; + }); + + afterAll(() => { + delete (window as any).IntersectionObserver; + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductComponent, TranslateModule.forRoot()], + providers: [ + { + provide: Router, + useValue: router + }, + ProductService, + TranslateService, + provideHttpClient() + ] + }) + .overrideComponent(ProductComponent, { + remove: { providers: [ProductService] }, + add: { + providers: [{ provide: ProductService, useClass: MockProductService }] + } + }) + .compileComponents(); + fixture = TestBed.createComponent(ProductComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('viewProductDetail should navigate', () => { + component.viewProductDetail('url'); + expect(router.navigate).toHaveBeenCalledWith(['', 'url']); + }); + + it('loadProductItems should return products with criteria', () => { + + component.loadProductItems(); + expect(component.loadProductItems).toBeTruthy(); + }) + + it('ngOnDestroy should unsubscribe all sub', () => { + const sub = new Subscription(); + component.subscriptions.push(sub); + component.ngOnDestroy(); + expect(component.ngOnDestroy).toBeTruthy(); + }); + + it('onFilterChange should filter products properly', () => { + component.onFilterChange(TypeOption.CONNECTORS); + component.products().forEach((product) => { + expect(product.type).toEqual('connector'); + }); + }); + + it('onSortChange should order products properly', () => { + component.onSearchChanged('cur'); + component.onSortChange(SortOption.ALPHABETICALLY); + for (let i = 0; i < component.products.length - 1; i++) { + expect( + component.products()[i + 1].names.en.localeCompare(component.products()[i].names.en) + ).toEqual(1); + } + }); + + it('search should return match products name', fakeAsync(() => { + const productName = 'amazon comprehend'; + component.onSearchChanged(productName); + tick(500); + component.products().forEach((product) => { + expect(product.names.en.toLowerCase()).toContain(productName); + }); + })); + + it('setupIntersectionObserver should not trigger when init page', () => { + component.ngAfterViewInit(); + expect(component.criteria.nextPageHref).toBeUndefined(); + }); + + it('should call loadProductItems when observerElement is intersecting and has more products', () => { + spyOn(component, 'loadProductItems').and.callThrough(); + spyOn(component, 'hasMore').and.returnValue(true); + + const entries = [{ isIntersecting: true }]; + const callback = mockIntersectionObserver.callback; + + callback(entries as IntersectionObserverEntry[]); + + expect(component.hasMore).toHaveBeenCalled(); + expect(component.loadProductItems).toHaveBeenCalled(); + }); + + it('should not call loadProductItems when observerElement is not intersecting', () => { + spyOn(component, 'loadProductItems').and.callThrough(); + spyOn(component, 'hasMore').and.returnValue(true); + + const entries = [{ isIntersecting: false }]; + const callback = mockIntersectionObserver.callback; + + callback(entries as IntersectionObserverEntry[]); + + expect(component.hasMore).not.toHaveBeenCalled(); + expect(component.loadProductItems).not.toHaveBeenCalled(); + }); + + it('should not call loadProductItems when there are no more products', () => { + spyOn(component, 'loadProductItems').and.callThrough(); + spyOn(component, 'hasMore').and.returnValue(false); + + const entries = [{ isIntersecting: true }]; + const callback = mockIntersectionObserver.callback; + + callback(entries as IntersectionObserverEntry[]); + + expect(component.hasMore).toHaveBeenCalled(); + expect(component.loadProductItems).not.toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product.component.ts b/marketplace-ui/src/app/modules/product/product.component.ts new file mode 100644 index 000000000..00ae84665 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.component.ts @@ -0,0 +1,155 @@ +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + Component, + ElementRef, + OnDestroy, + ViewChild, + WritableSignal, + inject, + signal +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Subject, Subscription, debounceTime } from 'rxjs'; +import { ThemeService } from '../../core/services/theme/theme.service'; +import { TypeOption } from '../../shared/enums/type-option.enum'; +import { SortOption } from '../../shared/enums/sort-option.enum'; +import { Criteria } from '../../shared/models/criteria.model'; +import { Product } from '../../shared/models/product.model'; +import { ProductCardComponent } from './product-card/product-card.component'; +import { ProductFilterComponent } from './product-filter/product-filter.component'; +import { ProductService } from './product.service'; +import { ProductApiResponse } from '../../shared/models/apis/product-response.model'; +import { Link } from '../../shared/models/apis/link.model'; +import { Page } from '../../shared/models/apis/page.model'; +import { Language } from '../../shared/enums/language.enum'; +import { LanguageService } from '../../core/services/language/language.service'; + +const SEARCH_DEBOUNCE_TIME = 500; + +@Component({ + selector: 'app-product', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TranslateModule, + ProductCardComponent, + ProductFilterComponent + ], + providers: [ProductService], + templateUrl: './product.component.html', + styleUrl: './product.component.scss' +}) +export class ProductComponent implements AfterViewInit, OnDestroy { + products: WritableSignal = signal([]); + subscriptions: Subscription[] = []; + searchTextChanged = new Subject(); + criteria: Criteria = { + search: '', + type: TypeOption.All_TYPES, + sort: SortOption.POPULARITY, + language: Language.EN + }; + responseLink!: Link; + responsePage!: Page; + + productService = inject(ProductService); + themeService = inject(ThemeService); + translateService = inject(TranslateService); + languageService = inject(LanguageService); + + router = inject(Router); + @ViewChild('observer', { static: true }) observerElement!: ElementRef; + + constructor() { + this.loadProductItems(); + this.subscriptions.push( + this.searchTextChanged + .pipe(debounceTime(SEARCH_DEBOUNCE_TIME)) + .subscribe(value => { + this.criteria = { + ...this.criteria, + search: value + }; + this.loadProductItems(true); + }) + ); + } + + ngAfterViewInit(): void { + this.setupIntersectionObserver(); + } + + viewProductDetail(productId: string) { + this.router.navigate(['', productId]); + } + + onFilterChange(selectedType: TypeOption) { + this.criteria = { + ...this.criteria, + nextPageHref: '', + type: selectedType + }; + this.loadProductItems(true); + } + + onSortChange(selectedSort: SortOption) { + this.criteria = { + ...this.criteria, + nextPageHref: '', + sort: selectedSort + }; + this.loadProductItems(true); + } + + onSearchChanged(searchString: string) { + this.searchTextChanged.next(searchString); + } + + loadProductItems(shouldCleanData = false) { + this.criteria.language = this.languageService.getSelectedLanguage(); + this.subscriptions.push( + this.productService.findProductsByCriteria(this.criteria).subscribe((response: ProductApiResponse) => { + const newProducts = response._embedded.products; + if (shouldCleanData) { + this.products.set(newProducts); + } else { + this.products.update(existingProducts => existingProducts.concat(newProducts)); + } + this.responseLink = response._links; + this.responsePage = response.page; + }) + ); + } + + setupIntersectionObserver() { + const options = { root: null, rootMargin: '0px', threshold: 0.1 }; + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting && this.hasMore()) { + this.criteria.nextPageHref = this.responseLink?.next?.href; + this.loadProductItems(); + } + }); + }, options); + + observer.observe(this.observerElement.nativeElement); + } + + hasMore() { + if (!this.responsePage || !this.responseLink) { + return false; + } + return this.responsePage.number < this.responsePage.totalPages + && this.responseLink?.next !== undefined; + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => { + sub.unsubscribe(); + }); + } +} diff --git a/marketplace-ui/src/app/modules/product/product.routes.ts b/marketplace-ui/src/app/modules/product/product.routes.ts new file mode 100644 index 000000000..c4fa88ebd --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.routes.ts @@ -0,0 +1,11 @@ +import { Route } from '@angular/router'; + +export const routes: Route[] = [ + { + path: '', + loadComponent: () => + import('./product-detail/product-detail.component').then( + (m) => m.ProductDetailComponent + ), + }, +]; diff --git a/marketplace-ui/src/app/modules/product/product.service.spec.ts b/marketplace-ui/src/app/modules/product/product.service.spec.ts new file mode 100644 index 000000000..df0deed1f --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.service.spec.ts @@ -0,0 +1,183 @@ +import { TestBed } from '@angular/core/testing'; + +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; +import { + HttpTestingController, + provideHttpClientTesting +} from '@angular/common/http/testing'; +import { TypeOption } from '../../shared/enums/type-option.enum'; +import { SortOption } from '../../shared/enums/sort-option.enum'; +import { MOCK_PRODUCTS } from '../../shared/mocks/mock-data'; +import { Criteria } from '../../shared/models/criteria.model'; +import { ProductService } from './product.service'; +import { Product } from '../../shared/models/product.model'; +import { catchError } from 'rxjs'; +import { LoadingService } from '../../core/services/loading/loading.service'; +import { VersionData } from '../../shared/models/vesion-artifact.model'; +import { Language } from '../../shared/enums/language.enum'; + +const PRODUCT_ID = 'amazon-comprehend'; +const NOT_EXIST_ID = 'undefined'; + +describe('ProductService', () => { + let products = MOCK_PRODUCTS._embedded.products as Product[]; + let service: ProductService; + let httpMock: HttpTestingController; + let loadingServiceSpy: jasmine.SpyObj; + + beforeEach(() => { + const spyLoading = jasmine.createSpyObj('LoadingService', ['show', 'hide']); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + ProductService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: LoadingService, useValue: spyLoading } + ] + }); + service = TestBed.inject(ProductService); + httpMock = TestBed.inject(HttpTestingController); + loadingServiceSpy = TestBed.inject( + LoadingService + ) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('getProductById should return a product', () => { + service.getProductById(PRODUCT_ID).subscribe(data => { + expect(data.id).toEqual(PRODUCT_ID); + }); + }); + + it('getProductById should return null product', () => { + service.getProductById(NOT_EXIST_ID).subscribe(data => { + expect(data).toEqual({} as Product); + }); + }); + + it('findProductsByCriteria with should return products properly', () => { + const searchString = 'Amazon Comprehend'; + const criteria: Criteria = { + search: searchString, + sort: SortOption.ALPHABETICALLY, + type: TypeOption.CONNECTORS, + language: Language.EN + }; + service.findProductsByCriteria(criteria).subscribe(response => { + let products = response._embedded.products; + for (let i = 0; i < products.length; i++) { + expect(products[i].type).toEqual(TypeOption.CONNECTORS); + expect(products[i].names.en.toLowerCase()).toContain(searchString); + if (products[i + 1]) { + expect(products[i + 1].names.en.localeCompare(products[i].names.en)).toEqual( + 1 + ); + } + } + }); + }); + + it('findProductsByCriteria with empty searchString', () => { + const criteria: Criteria = { + search: '', + sort: null, + type: null, + language: Language.EN + }; + service.findProductsByCriteria(criteria).subscribe(response => { + expect(response._embedded.products.length).toEqual(products.length); + }); + }); + + it('findProductsByCriteria with popularity order', () => { + const criteria: Criteria = { + search: '', + sort: SortOption.POPULARITY, + type: null, + language: Language.EN + }; + service.findProductsByCriteria(criteria).subscribe(response => { + let products = response._embedded.products; + for (let i = 0; i < products.length; i++) { + if ( + products[i].platformReview && + products[i + 1] && + products[i + 1].platformReview + ) { + expect(Number(products[i + 1].platformReview)).toBeGreaterThanOrEqual( + Number(products[i].platformReview) + ); + } + } + }); + }); + + it('findProductsByCriteria with default sort', () => { + const criteria: Criteria = { + search: '', + sort: SortOption.RECENT, + type: null, + language: Language.EN + }; + service.findProductsByCriteria(criteria).subscribe(response => { + expect(response._embedded.products.length).toEqual(products.length); + }); + }); + + it('findProductsByCriteria by next page url', () => { + const criteria: Criteria = { + nextPageHref: + 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20', + search: '', + sort: SortOption.RECENT, + type: TypeOption.All_TYPES, + language: Language.EN + }; + service.findProductsByCriteria(criteria).subscribe(response => { + expect(response._embedded.products.length).toEqual(0); + expect(response.page.number).toEqual(1); + }); + }); + + it('should call the API and return VersionData[]', () => { + const mockResponse: VersionData[] = [ + { version: '10.0.1', artifactsByVersion: [] } + ]; + + const productId = 'adobe-acrobat-connector'; + const showDevVersion = true; + const designerVersion = '10.0.1'; + + service + .sendRequestToProductDetailVersionAPI( + productId, + showDevVersion, + designerVersion + ) + .subscribe(data => { + expect(data).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(request => { + return ( + request.url === `api/product-details/${productId}/versions` && + request.params.get('designerVersion') === designerVersion && + request.params.get('isShowDevVersion') === showDevVersion.toString() + ); + }); + + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + + expect(loadingServiceSpy.show).toHaveBeenCalled(); + expect(loadingServiceSpy.hide).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product.service.ts b/marketplace-ui/src/app/modules/product/product.service.ts new file mode 100644 index 000000000..fac5ed24c --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product.service.ts @@ -0,0 +1,62 @@ +import { HttpClient, HttpContext, HttpParams } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { Observable, of, tap } from 'rxjs'; +import { MOCK_PRODUCTS } from '../../shared/mocks/mock-data'; +import { Criteria } from '../../shared/models/criteria.model'; +import { Product } from '../../shared/models/product.model'; +import { VersionData } from '../../shared/models/vesion-artifact.model'; +import { LoadingService } from '../../core/services/loading/loading.service'; +import { RequestParam } from '../../shared/enums/request-param'; +import { ProductApiResponse } from '../../shared/models/apis/product-response.model'; +import { SkipLoading } from '../../core/interceptors/api.interceptor'; + +const PRODUCT_API_URL = 'api/product'; +@Injectable() +export class ProductService { + httpClient = inject(HttpClient); + loadingService = inject(LoadingService); + + findProductsByCriteria(criteria: Criteria): Observable { + let requestParams = new HttpParams(); + let requestURL = PRODUCT_API_URL; + if (criteria.nextPageHref) { + requestURL = criteria.nextPageHref; + } else { + requestParams = requestParams + .set(RequestParam.TYPE, `${criteria.type}`) + .set(RequestParam.SORT, `${criteria.sort}`) + .set(RequestParam.KEYWORD, `${criteria.search}`) + .set(RequestParam.LANGUAGE, `${criteria.language}`); + } + return this.httpClient.get(requestURL, { + params: requestParams, + context: new HttpContext().set(SkipLoading, true) + }); + } + + getProductById(productId: string): Observable { + const products = MOCK_PRODUCTS._embedded.products; + const product = products.find(p => p.id === productId); + if (product) { + return of(product); + } + return of({} as Product); + } + + sendRequestToProductDetailVersionAPI( + productId: string, + showDevVersion: boolean, + designerVersion: string + ): Observable { + this.loadingService.show(); + const url = `api/product-details/${productId}/versions`; + const params = new HttpParams() + .append('designerVersion', designerVersion) + .append('isShowDevVersion', showDevVersion); + return this.httpClient.get(url, { params }).pipe( + tap(() => { + this.loadingService.hide(); + }) + ); + } +} diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.html b/marketplace-ui/src/app/shared/components/footer/footer.component.html new file mode 100644 index 000000000..53941ff9e --- /dev/null +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.html @@ -0,0 +1,89 @@ + +
+ +
+
+ + +
+
+ + +
+
+ + + +
+ +
+ + + +
+
+ diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.scss b/marketplace-ui/src/app/shared/components/footer/footer.component.scss new file mode 100644 index 000000000..85e4d8669 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.scss @@ -0,0 +1,38 @@ +.logo { + margin: 0 0 2rem 0; +} + +.logo__image { + width: 132px; + height: 20px; + margin-top: 1rem; + vertical-align: initial; +} + +.nav-item { + margin: 0 0 0 30px; +} + +.nav-link { + font-weight: 600; + padding: 0 15px; +} + +.button-container { + margin: 24px 0 0 0; +} + +.link-icon { + width: 18px; + height: 18px; +} + +.link--left { + margin: 0 2rem 0 0; +} + +@media all and (max-width: 990px) { + .link--left { + margin: 0; + } +} diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.spec.ts b/marketplace-ui/src/app/shared/components/footer/footer.component.spec.ts new file mode 100644 index 000000000..5c276dfd6 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FooterComponent } from './footer.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; + +declare const viewport: Viewport; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FooterComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('navbar should not display in mobile screen', () => { + viewport.set(540); + + const mobileSearch = fixture.debugElement.query(By.css('.footer__navbar')); + + expect(getComputedStyle(mobileSearch.nativeElement).display).toBe('none'); + }); + + it('social media section should be in the bottom of mobile screen', () => { + viewport.set(540); + + const footerSocialMedia = fixture.nativeElement.querySelector( + '.footer__social-media' + ); + const footerIvyPolicy = fixture.nativeElement.querySelector( + '.footer__ivy-policy' + ); + + expect(footerSocialMedia.getBoundingClientRect().top).toBeGreaterThan( + footerIvyPolicy.getBoundingClientRect().top + ); + }); + + it('Ivy tag in ivy policy section should be display in higher row', () => { + viewport.set(540); + + const ivyTag = fixture.nativeElement.querySelector('.footer__ivy-tag'); + + const ivyTermOfService = fixture.nativeElement.querySelector( + '.footer__ivy-term-of-service-tag' + ); + + expect(ivyTag.getBoundingClientRect().top).toBeLessThan( + ivyTermOfService.getBoundingClientRect().top + ); + }); + + it('content layout should be displayed in the center', () => { + viewport.set(480); + + const logo = fixture.debugElement.query(By.css('.logo__image')); + const ivyPolicy = fixture.debugElement.query(By.css('.footer__ivy-policy')); + expect(getComputedStyle(logo.nativeElement).textAlign).toBe('center'); + expect(getComputedStyle(ivyPolicy.nativeElement).textAlign).toBe('center'); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.ts b/marketplace-ui/src/app/shared/components/footer/footer.component.ts new file mode 100644 index 000000000..4a3d088c0 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ThemeService } from '../../../core/services/theme/theme.service'; +import { + IVY_FOOTER_LINKS, + NAV_ITEMS, + SOCIAL_MEDIA_LINK +} from '../../constants/common.constant'; +import { NavItem } from '../../models/nav-item.model'; + +@Component({ + selector: 'app-footer', + standalone: true, + imports: [CommonModule, TranslateModule], + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss', '../../../app.component.scss'] +}) +export class FooterComponent { + themeService = inject(ThemeService); + socialMediaLinks = SOCIAL_MEDIA_LINK; + navItems: NavItem[] = NAV_ITEMS; + ivyFooterLinks = IVY_FOOTER_LINKS; +} diff --git a/marketplace-ui/src/app/shared/components/header/header.component.html b/marketplace-ui/src/app/shared/components/header/header.component.html new file mode 100644 index 000000000..b589d0446 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/header.component.html @@ -0,0 +1,38 @@ + diff --git a/marketplace-ui/src/app/shared/components/header/header.component.scss b/marketplace-ui/src/app/shared/components/header/header.component.scss new file mode 100644 index 000000000..f05d6844e --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/header.component.scss @@ -0,0 +1,59 @@ +.navbar { + opacity: 0px; +} + +.navbar-brand { + padding-top: 0.8rem; +} + +.logo__image { + width: 130.5px; + height: 20px; + vertical-align: initial; +} + +.header__menu-button { + font-size: 24px; +} + +@media all and (max-width: 992px) { + .show, + .min-height-100 { + min-height: 85lvh; + } + + .form-control { + height: 43px; + } + + .navbar { + border: none; + } + + .navbar-brand { + padding: 14.42px 0; + } + + .nav-link { + height: 45px; + padding: 10px 0px 10px 15px; + gap: 0px; + border-radius: 5px; + opacity: 0px; + } + + .nav-link.active { + height: 45px; + padding: 10px 15px 10px -15px; + gap: 0px; + border-radius: 5px; + opacity: 0px; + background-color: var(--ivy-secondary-background); + } +} + +@media all and (max-width: 1200px) { + .header__navbar-content { + height: 90vh; + } +} diff --git a/marketplace-ui/src/app/shared/components/header/header.component.spec.ts b/marketplace-ui/src/app/shared/components/header/header.component.spec.ts new file mode 100644 index 000000000..72f6688f7 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/header.component.spec.ts @@ -0,0 +1,105 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { HeaderComponent } from './header.component'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; + +declare const viewport: Viewport; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HeaderComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle the mobile menu on click', () => { + const navbarToggler = fixture.debugElement.query(By.css('.bi.bi-list')); + + expect(component.isMobileMenuCollapsed()).toBeTrue(); + + // Click the mobile menu toggler + navbarToggler.triggerEventHandler('click', null); + fixture.detectChanges(); + + expect(component.isMobileMenuCollapsed()).toBeFalse(); + + // Click the mobile menu toggler again + navbarToggler.triggerEventHandler('click', null); + fixture.detectChanges(); + + expect(component.isMobileMenuCollapsed()).toBeTrue(); + }); + + // Responsive section + it('action section should display in the bottom of the view in mobile mode', () => { + viewport.set(540); + + const headerNavigation = fixture.nativeElement.querySelector( + '.header__navigation' + ); + const headerAction = fixture.nativeElement.querySelector('.header__action'); + + const headerNavigationBeforeShowNavBar = + headerNavigation.getBoundingClientRect(); + const headerActionBeforeShowNavBar = headerAction.getBoundingClientRect(); + + const menuButton = fixture.debugElement.query( + By.css('.header__menu-button') + ); + menuButton.triggerEventHandler('click', null); + fixture.detectChanges(); + const headerNavigationAfterShowNavBar = + headerNavigation.getBoundingClientRect(); + const headerActionAfterShowNavBar = headerAction.getBoundingClientRect(); + expect(headerNavigationBeforeShowNavBar.top).toBeLessThan( + headerActionAfterShowNavBar.top + ); + expect(headerActionBeforeShowNavBar.top).toBeLessThan( + headerNavigationAfterShowNavBar.top + ); + + expect(headerNavigationAfterShowNavBar.bottom).toBeLessThan( + headerActionAfterShowNavBar.top + ); + }); + + it('navigation section should display in vertical', () => { + viewport.set(540); + const menuButton = fixture.debugElement.query( + By.css('.header__menu-button') + ); + menuButton.triggerEventHandler('click', null); + + fixture.detectChanges(); + const navBar = fixture.debugElement.query( + By.css('.header__navbar-content') + ); + + expect(getComputedStyle(navBar.nativeElement).flexDirection).toBe('column'); + }); + + it('menu button should be in the right side of mobile view', () => { + viewport.set(540); + const menuButton = fixture.nativeElement.querySelector( + '.header__menu-button' + ); + + const logo = fixture.nativeElement.querySelector('.logo__image'); + expect(menuButton.getBoundingClientRect().left).toBeGreaterThan( + logo.getBoundingClientRect().right + ); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/header/header.component.ts b/marketplace-ui/src/app/shared/components/header/header.component.ts new file mode 100644 index 000000000..e4d84e495 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/header.component.ts @@ -0,0 +1,47 @@ +import { CommonModule } from '@angular/common'; +import { Component, WritableSignal, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { LanguageService } from '../../../core/services/language/language.service'; +import { ThemeService } from '../../../core/services/theme/theme.service'; +import { Language } from '../../enums/language.enum'; +import { LanguageSelectionComponent } from './language-selection/language-selection.component'; +import { NavigationComponent } from './navigation/navigation.component'; +import { SearchBarComponent } from './search-bar/search-bar.component'; +import { ThemeSelectionComponent } from './theme-selection/theme-selection.component'; + +@Component({ + selector: 'app-header', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TranslateModule, + NavigationComponent, + ThemeSelectionComponent, + LanguageSelectionComponent, + SearchBarComponent + ], + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss', '../../../app.component.scss'] +}) +export class HeaderComponent { + selectedNav = '/'; + selectedLanguage = Language.EN; + + isMobileMenuCollapsed: WritableSignal = signal(true); + + themeService = inject(ThemeService); + translateService = inject(TranslateService); + languageService = inject(LanguageService); + + constructor() { + this.selectedLanguage = this.languageService.getSelectedLanguage(); + this.translateService.setDefaultLang(this.selectedLanguage); + this.translateService.use(this.selectedLanguage); + } + + onCollapsedMobileMenu() { + this.isMobileMenuCollapsed.update(value => !value); + } +} diff --git a/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.html b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.html new file mode 100644 index 000000000..6269cbb0b --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.html @@ -0,0 +1,14 @@ +
+ @for (language of languages; track $index) { +
+ {{ language.label }} +
+ } +
diff --git a/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.scss b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.scss new file mode 100644 index 000000000..a9b5a469d --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.scss @@ -0,0 +1,18 @@ +.language-item { + width: 22px; + height: 22px; + font-size: 16px; + text-align: center; + font-weight: 600; + margin: 0 0 0 10px; + background: none; + cursor: pointer; +} + +.language-item.active { + color: var(--ivy-active-color); +} + +.language-item.inactive { + color: #757575; +} diff --git a/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.spec.ts b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.spec.ts new file mode 100644 index 000000000..a11d1f541 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; + +import { LanguageSelectionComponent } from './language-selection.component'; + +describe('LanguageSelectionComponent', () => { + let component: LanguageSelectionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LanguageSelectionComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(LanguageSelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('selectLanguage should call translateService', () => { + spyOn(component.translateService, 'use').and.stub(); + component.onSelectLanguage('en'); + expect(component.translateService.use).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.ts b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.ts new file mode 100644 index 000000000..4edc57603 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/language-selection/language-selection.component.ts @@ -0,0 +1,27 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + inject +} from '@angular/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { LANGUAGES } from '../../../constants/common.constant'; +import { LanguageService } from '../../../../core/services/language/language.service'; + +@Component({ + selector: 'app-language-selection', + standalone: true, + imports: [CommonModule, TranslateModule], + templateUrl: './language-selection.component.html', + styleUrl: './language-selection.component.scss' +}) +export class LanguageSelectionComponent { + languages = LANGUAGES; + translateService = inject(TranslateService); + languageService = inject(LanguageService); + + onSelectLanguage(language: string) { + this.translateService.setDefaultLang(language); + this.translateService.use(language); + this.languageService.loadLanguage(language); + } +} diff --git a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html new file mode 100644 index 000000000..1b4f513c6 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html @@ -0,0 +1,36 @@ +
+ + + + + +
diff --git a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.scss b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.scss new file mode 100644 index 000000000..ae3aca955 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.scss @@ -0,0 +1,61 @@ +.navbar-nav { + height: 60px; + padding-top: 0.8rem; +} + +.nav-item { + padding: 0 4px; + margin: 0 1rem 0 0; +} + +.nav-link { + height: 22px; + line-height: 22.4px; + font-weight: 400; + font-size: 16px; +} + +.active-line { + position: relative; + height: 2px; + top: 2rem; + width: inherit; + background-color: var(--ivy-active-color); +} + +.header-mobile__search { + height: 43px; + border-radius: 10px; +} + +@media all and (max-width: 992px) { + .show, + .min-height-100 { + min-height: 85lvh; + } + + .form-control { + height: 43px; + } + + .navbar { + border: none; + } + + .nav-link { + height: 45px; + padding: 10px 0px 10px 15px; + gap: 0px; + border-radius: 5px; + opacity: 0px; + } + + .nav-link.active { + height: 45px; + padding: 10px 15px 10px -15px; + gap: 0px; + border-radius: 5px; + opacity: 0px; + background-color: var(--ivy-secondary-background); + } +} diff --git a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.spec.ts b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.spec.ts new file mode 100644 index 000000000..2370b4f25 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { NavigationComponent } from './navigation.component'; +declare const viewport: Viewport; + +describe('NavigationComponent', () => { + let component: NavigationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NavigationComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(NavigationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('mobile search should display in small screen', () => { + viewport.set(540); + + const mobileSearch = fixture.debugElement.query( + By.css('.header-mobile__search') + ); + + expect(getComputedStyle(mobileSearch.nativeElement).display).not.toBe( + 'none' + ); + }); + + it('mobile search should not display in large screen', () => { + viewport.set(1920); + + const mobileSearch = fixture.debugElement.query( + By.css('.header-mobile__search') + ); + + expect(getComputedStyle(mobileSearch.nativeElement).display).toBe('none'); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts new file mode 100644 index 000000000..f03bf7198 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, inject } from '@angular/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { NAV_ITEMS } from '../../../constants/common.constant'; +import { NavItem } from '../../../models/nav-item.model'; + +@Component({ + selector: 'app-navigation', + standalone: true, + imports: [CommonModule, TranslateModule], + templateUrl: './navigation.component.html', + styleUrl: './navigation.component.scss' +}) +export class NavigationComponent { + @Input() navItems: NavItem[] = NAV_ITEMS; + + translateService = inject(TranslateService); +} diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html new file mode 100644 index 000000000..e7c2fd853 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html @@ -0,0 +1,81 @@ + diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.scss b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.scss new file mode 100644 index 000000000..e5650f91d --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.scss @@ -0,0 +1,33 @@ +.search-icon { + width: 22px; + height: 22px; + margin: 0 16px 0 16px; +} + +.bi-search { + font-size: 16.5px; +} + +.input-group { + border-radius: 10px; +} + +.header__search-input { + height: 37px; +} + +.header__download-button { + height: 35px; + border-radius: 7px; +} + +@media all and (max-width: 992px) { + .header__button { + padding: 10px 0; + margin: 20px 0 0 0; + } + + .border-top-md-gray { + border-top: 1px solid var(--header-border-color); + } +} diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.spec.ts b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.spec.ts new file mode 100644 index 000000000..11c8cb238 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.spec.ts @@ -0,0 +1,74 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { SearchBarComponent } from './search-bar.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; + +declare const viewport: Viewport; + +describe('SearchBarComponent', () => { + let component: SearchBarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchBarComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(SearchBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle the search input visibility on search icon click', () => { + viewport.set(1920); + const searchIcon = fixture.debugElement.query( + By.css('.header__search-button') + ); + + expect(component.isSearchBarDisplayed()).toBeFalse(); + + // Click the search icon + searchIcon.triggerEventHandler('click', null); + fixture.detectChanges(); + + expect(component.isSearchBarDisplayed()).toBeTrue(); + + const cancelIcon = fixture.debugElement.query( + By.css('.input-group-prepend.search__cancel-button') + ); + + // Click the cancel icon + cancelIcon.triggerEventHandler('click', null); + fixture.detectChanges(); + + expect(component.isSearchBarDisplayed()).toBeFalse(); + }); + + it('destop search should not display in small screen', () => { + viewport.set(540); + + const desktopSearch = fixture.debugElement.query( + By.css('.header__search-button') + ); + + expect(desktopSearch).toBeNull; + }); + + it('desktop search should display in large screen', () => { + viewport.set(1920); + + const desktopSearch = fixture.debugElement.query( + By.css('.header__search-button') + ); + + expect(getComputedStyle(desktopSearch.nativeElement).display).not.toBe( + 'none' + ); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts new file mode 100644 index 000000000..2565dbb1a --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts @@ -0,0 +1,50 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + Output, + inject, + signal +} from '@angular/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { LanguageSelectionComponent } from '../language-selection/language-selection.component'; +import { ThemeSelectionComponent } from '../theme-selection/theme-selection.component'; + +@Component({ + selector: 'app-search-bar', + standalone: true, + imports: [ + CommonModule, + TranslateModule, + ThemeSelectionComponent, + LanguageSelectionComponent + ], + templateUrl: './search-bar.component.html', + styleUrl: './search-bar.component.scss' +}) +export class SearchBarComponent { + @Input() isSearchBarDisplayed = signal(false); + @Output() isShowSearchBarChange = new EventEmitter(); + + translateService = inject(TranslateService); + + elementRef = inject(ElementRef); + + @HostListener('document:click', ['$event']) + handleClickOutside(event: MouseEvent) { + if (!this.elementRef.nativeElement.contains(event.target)) { + this.isSearchBarDisplayed.set(false); + } + } + + onClickSearchIcon() { + this.isSearchBarDisplayed.set(true); + } + + onHideSearch() { + this.isSearchBarDisplayed.set(false); + } +} diff --git a/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.html b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.html new file mode 100644 index 000000000..f5f1e5e3e --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.html @@ -0,0 +1,9 @@ +
+ + @if (themeService.isDarkMode()) { + + } @else { + + } + +
diff --git a/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.scss b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.scss new file mode 100644 index 000000000..4db4a38be --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.scss @@ -0,0 +1,10 @@ +.header__theme-button { + height: 22px; + width: 22px; + margin: 0 0 0 16px; + text-align: center; +} + +i { + font-size: 16.5px; +} diff --git a/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.spec.ts b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.spec.ts new file mode 100644 index 000000000..c0052ec79 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ThemeSelectionComponent } from './theme-selection.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +describe('ThemeSelectionComponent', () => { + let component: ThemeSelectionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ThemeSelectionComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(ThemeSelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle the theme on theme button click', () => { + spyOn(component.themeService, 'changeTheme').and.callThrough(); + const themeButton = fixture.debugElement.query( + By.css('.header__theme-button') + ); + + // Click the theme button + themeButton.triggerEventHandler('click', null); + fixture.detectChanges(); + + expect(component.themeService.changeTheme).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.ts b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.ts new file mode 100644 index 000000000..aca45df49 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/header/theme-selection/theme-selection.component.ts @@ -0,0 +1,13 @@ +import { Component, inject } from '@angular/core'; +import { ThemeService } from '../../../../core/services/theme/theme.service'; + +@Component({ + selector: 'app-theme-selection', + standalone: true, + imports: [], + templateUrl: './theme-selection.component.html', + styleUrl: './theme-selection.component.scss' +}) +export class ThemeSelectionComponent { + themeService = inject(ThemeService); +} diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts new file mode 100644 index 000000000..968d80da2 --- /dev/null +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -0,0 +1,110 @@ +import { TypeOption } from '../enums/type-option.enum'; +import { Language } from '../enums/language.enum'; +import { SortOption } from '../enums/sort-option.enum'; +import { NavItem } from '../models/nav-item.model'; + +export const NAV_ITEMS: NavItem[] = [ + { + label: 'common.nav.news', + link: 'https://developer.axonivy.com/news' + }, + { + label: 'common.nav.doc', + link: 'https://developer.axonivy.com/doc' + }, + { + label: 'common.nav.tutorial', + link: 'https://developer.axonivy.com/tutorial' + }, + { + label: 'common.nav.community', + link: 'https://community.axonivy.com/' + }, + { + label: 'common.nav.team', + link: 'https://developer.axonivy.com/team' + }, + { + label: 'common.nav.market', + link: '/' + } +]; + +export const SOCIAL_MEDIA_LINK = [ + { + styleClass: 'fab fa-linkedin', + url: '/' + }, + { + styleClass: 'fab fa-xing', + url: '/' + }, + { + styleClass: 'fab fa-youtube', + url: '/' + }, + { + styleClass: 'fab fa-facebook', + url: '/' + } +]; + +export const IVY_FOOTER_LINKS = [ + { + containerStyleClass: 'w-md-100 footer__ivy-tag', + label: 'common.footer.ivyCompanyInfo' + }, + { + containerStyleClass: 'footer__ivy-policy-tag', + label: 'common.footer.privacyPolicy' + }, + { + containerStyleClass: 'footer__ivy-term-of-service-tag', + label: 'common.footer.termsOfService' + } +]; + +export const LANGUAGES = [ + { + value: Language.DE, + label: 'DE' + }, + { + value: Language.EN, + label: 'EN' + } +]; + +export const FILTER_TYPES = [ + { + value: TypeOption.All_TYPES, + label: 'common.filter.value.allTypes' + }, + { + value: TypeOption.CONNECTORS, + label: 'common.filter.value.connector' + }, + { + value: TypeOption.UTILITIES, + label: 'common.filter.value.util' + }, + { + value: TypeOption.SOLUTION, + label: 'common.filter.value.solution' + } +]; + +export const SORT_TYPES = [ + { + value: SortOption.POPULARITY, + label: 'common.sort.value.popularity' + }, + { + value: SortOption.ALPHABETICALLY, + label: 'common.sort.value.alphabetically' + }, + { + value: SortOption.RECENT, + label: 'common.sort.value.recent' + } +]; diff --git a/marketplace-ui/src/app/shared/enums/language.enum.ts b/marketplace-ui/src/app/shared/enums/language.enum.ts new file mode 100644 index 000000000..7d38c08f1 --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/language.enum.ts @@ -0,0 +1,4 @@ +export enum Language { + EN = 'en', + DE = 'de' +} diff --git a/marketplace-ui/src/app/shared/enums/request-param.ts b/marketplace-ui/src/app/shared/enums/request-param.ts new file mode 100644 index 000000000..207ea34f9 --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/request-param.ts @@ -0,0 +1,6 @@ +export enum RequestParam { + TYPE = 'type', + KEYWORD = 'keyword', + SORT = 'sort', + LANGUAGE = 'language' +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/enums/sort-option.enum.ts b/marketplace-ui/src/app/shared/enums/sort-option.enum.ts new file mode 100644 index 000000000..d2b3e179f --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/sort-option.enum.ts @@ -0,0 +1,5 @@ +export enum SortOption { + POPULARITY = 'popularity', + ALPHABETICALLY = 'alphabetically', + RECENT = 'recent' +} diff --git a/marketplace-ui/src/app/shared/enums/theme.enum.ts b/marketplace-ui/src/app/shared/enums/theme.enum.ts new file mode 100644 index 000000000..6d4c78a96 --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/theme.enum.ts @@ -0,0 +1,4 @@ +export enum Theme { + LIGHT = 'light', + DARK = 'dark', +} diff --git a/marketplace-ui/src/app/shared/enums/type-option.enum.ts b/marketplace-ui/src/app/shared/enums/type-option.enum.ts new file mode 100644 index 000000000..beb0ecf6b --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/type-option.enum.ts @@ -0,0 +1,7 @@ +export enum TypeOption { + All_TYPES = 'all', + CONNECTORS = 'connectors', + UTILITIES = 'utilities', + DEMOS = 'demos', + SOLUTION = 'solutions' +} diff --git a/marketplace-ui/src/app/shared/mocks/mock-data.ts b/marketplace-ui/src/app/shared/mocks/mock-data.ts new file mode 100644 index 000000000..e8e43065d --- /dev/null +++ b/marketplace-ui/src/app/shared/mocks/mock-data.ts @@ -0,0 +1,209 @@ +import { ProductApiResponse } from "../models/apis/product-response.model"; + +export const MOCK_PRODUCTS = { + _embedded: { + products: [ + { + id: "amazon-comprehend", + names: { + en: "Amazon Comprehend", + de: "TODO Amazon Comprehend" + }, + shortDescriptions: { + en: "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", + de: "Amazon Comprehend ist ein KI-Service, der maschinelles Lernen nutzt, um aus unstrukturierten Daten wertvolle Informationen zu generieren." + }, + logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend/logo.png", + type: "connector", + tags: [ + "AI" + ], + _links: { + self: { + href: "http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector" + } + } + } + ] + }, + _links: { + first: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + self: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + next: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + }, + last: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20" + } + }, + page: { + size: 20, + totalElements: 70, + totalPages: 4, + number: 0 + } +} as ProductApiResponse; + +export const MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS = { + _embedded: { + products: [ + { + id: "amazon-comprehend", + names: { + en: "Amazon Comprehend", + de: "" + }, + shortDescriptions: { + en: "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", + de: "" + }, + logoUrl: "", + type: "connector", + tags: [ + "AI" + ], + _links: { + self: { + href: "http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector" + } + } + } + ] + }, + _links: { + first: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + self: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + next: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + }, + last: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20" + } + }, + page: { + size: 20, + totalElements: 70, + totalPages: 4, + number: 0 + } +} as ProductApiResponse; + +export const MOCK_PRODUCTS_FILTER_CONNECTOR = { + _embedded: { + products: [ + { + id: "amazon-comprehend", + names: { + en: "Amazon Comprehend", + de: "TODO Amazon Comprehend" + }, + shortDescriptions: { + en: "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", + de: "Amazon Comprehend ist ein KI-Service, der maschinelles Lernen nutzt, um aus unstrukturierten Daten wertvolle Informationen zu generieren." + }, + logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend/logo.png", + type: "connector", + tags: [ + "AI" + ], + _links: { + self: { + href: "http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector" + } + } + }, + { + id: "a-trust", + names: { + en: "A-Trust", + de: "A-Trust" + }, + shortDescriptions: { + en: "Clearly authenticate your Austrian customers with a mobile phone signature.", + de: "Clearly authenticate your Austrian customers with a mobile phone signature." + }, + logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/a-trust/logo.png", + type: "connector", + tags: [ + "e-signature" + ], + _links: { + self: { + href: "http://localhost:8080/marketplace-service/api/product-details/a-trust?type=connector" + } + } + }, + { + id: "mailstore-connector", + names: { + en: "Mailstore", + de: "Mailstore" + }, + shortDescriptions: { + en: "Enhance business processes by streamlining email management, supporting both IMAP and POP3 with robust SSL encryption.", + de: "Enhance business processes by streamlining email management, supporting both IMAP and POP3 with robust SSL encryption." + }, + logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/mailstore-connector/logo.png", + type: "connector", + tags: [ + "office", + "email" + ], + _links: { + self: { + href: "http://localhost:8080/marketplace-service/api/product-details/mailstore-connector?type=connector" + } + } + } + ] + }, + _links: { + first: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + self: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + next: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + }, + last: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20" + } + }, + page: { + size: 20, + totalElements: 70, + totalPages: 4, + number: 0 + } +} as ProductApiResponse; + + +export const MOCK_PRODUCTS_NEXT_PAGE = { + _embedded: { + products: [] + }, + _links: { + first: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + self: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + } + }, + page: { + size: 20, + totalElements: 1, + totalPages: 1, + number: 1 + } +} as ProductApiResponse; \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/mocks/mock-services.ts b/marketplace-ui/src/app/shared/mocks/mock-services.ts new file mode 100644 index 000000000..e7640bdfb --- /dev/null +++ b/marketplace-ui/src/app/shared/mocks/mock-services.ts @@ -0,0 +1,24 @@ +import { Observable, of } from 'rxjs'; +import { Product } from '../models/product.model'; +import { Criteria } from '../models/criteria.model'; +import { TypeOption } from '../enums/type-option.enum'; +import { MOCK_PRODUCTS, MOCK_PRODUCTS_FILTER_CONNECTOR, MOCK_PRODUCTS_NEXT_PAGE } from './mock-data'; +import { ProductApiResponse } from '../models/apis/product-response.model'; + +const products = MOCK_PRODUCTS._embedded.products as Product[]; +export class MockProductService { + + getProductById(id: string) { + return of(products.find(product => product.id === id)); + } + + findProductsByCriteria(criteria: Criteria): Observable { + let response = MOCK_PRODUCTS; + if (criteria.nextPageHref) { + response = MOCK_PRODUCTS_NEXT_PAGE; + } else if (criteria.type == TypeOption.CONNECTORS) { + response = MOCK_PRODUCTS_FILTER_CONNECTOR; + } + return of(response); + } +} diff --git a/marketplace-ui/src/app/shared/models/apis/link.model.ts b/marketplace-ui/src/app/shared/models/apis/link.model.ts new file mode 100644 index 000000000..057f5af38 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/apis/link.model.ts @@ -0,0 +1,14 @@ +export interface Link { + self: { + href: string; + }; + first?: { + href: string; + }; + next?: { + href: string; + }; + last?: { + href: string; + }; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/models/apis/page.model.ts b/marketplace-ui/src/app/shared/models/apis/page.model.ts new file mode 100644 index 000000000..15b585275 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/apis/page.model.ts @@ -0,0 +1,6 @@ +export interface Page { + size: number; + totalElements: number; + totalPages: number; + number: number; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/models/apis/product-response.model.ts b/marketplace-ui/src/app/shared/models/apis/product-response.model.ts new file mode 100644 index 000000000..d9020d593 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/apis/product-response.model.ts @@ -0,0 +1,11 @@ +import { Product } from "../product.model"; +import { Link } from "./link.model"; +import { Page } from "./page.model"; + +export interface ProductApiResponse { + _embedded: { + products: Product[]; + }; + _links: Link; + page: Page; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/models/criteria.model.ts b/marketplace-ui/src/app/shared/models/criteria.model.ts new file mode 100644 index 000000000..fce18f832 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/criteria.model.ts @@ -0,0 +1,10 @@ +import { Language } from "../enums/language.enum"; +import { SortOption } from "../enums/sort-option.enum"; +import { TypeOption } from "../enums/type-option.enum"; +export interface Criteria { + search: string; + sort: SortOption | null; + type: TypeOption | null; + language: Language; + nextPageHref?: string; +} diff --git a/marketplace-ui/src/app/shared/models/display-value.model.ts b/marketplace-ui/src/app/shared/models/display-value.model.ts new file mode 100644 index 000000000..84c9d4c2a --- /dev/null +++ b/marketplace-ui/src/app/shared/models/display-value.model.ts @@ -0,0 +1,5 @@ +export interface DisplayValue { + en: string, + de: string +} + diff --git a/marketplace-ui/src/app/shared/models/maven-artifact.model.ts b/marketplace-ui/src/app/shared/models/maven-artifact.model.ts new file mode 100644 index 000000000..89906806f --- /dev/null +++ b/marketplace-ui/src/app/shared/models/maven-artifact.model.ts @@ -0,0 +1,16 @@ +export interface ArchivedArtifact { + lastVersion: string; + groupId: string; + artifactId: string; +} + +export interface MavenArtifact { + repoUrl?: string; + key?: string; + name: string; + groupId: string; + artifactId: string; + archivedArtifacts?: ArchivedArtifact[]; + type?: string; + doc?: boolean; +} diff --git a/marketplace-ui/src/app/shared/models/nav-item.model.ts b/marketplace-ui/src/app/shared/models/nav-item.model.ts new file mode 100644 index 000000000..596a7c8e7 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/nav-item.model.ts @@ -0,0 +1,4 @@ +export interface NavItem { + label: string; + link: string; +} diff --git a/marketplace-ui/src/app/shared/models/product.model.ts b/marketplace-ui/src/app/shared/models/product.model.ts new file mode 100644 index 000000000..fe73c9b7f --- /dev/null +++ b/marketplace-ui/src/app/shared/models/product.model.ts @@ -0,0 +1,33 @@ +import { DisplayValue } from './display-value.model'; +import { MavenArtifact } from './maven-artifact.model'; + +export interface Product { + id: string; + version: string; + names: DisplayValue; + shortDescriptions: DisplayValue; + type: string; + logoUrl: string; + cost: string; + platformReview: string; + vendor: string; + vendorImage: string; + vendorUrl: string; + sourceUrl: string; + statusBadgeUrl: string; + language: string; + industry: string; + listed: boolean; + compatibility: string; + tags: string[]; + validate: boolean; + versionDisplay: string; + installMatcher: string; + mavenArtifacts: MavenArtifact[]; + contactUs: boolean; + _links?: { + self: { + href: string; + }; + }; +} diff --git a/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts b/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts new file mode 100644 index 000000000..ab3f74b25 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts @@ -0,0 +1,10 @@ +export interface Artifact { + name: string; + downloadUrl: string; + isProductArtifact: boolean | null; +} + +export interface VersionData { + version: string; + artifactsByVersion: Artifact[]; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/pipes/logo.pipe.ts b/marketplace-ui/src/app/shared/pipes/logo.pipe.ts new file mode 100644 index 000000000..93554c3e6 --- /dev/null +++ b/marketplace-ui/src/app/shared/pipes/logo.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Product } from '../models/product.model'; + +@Pipe({ + standalone: true, + name: 'logo' +}) +export class ProductLogoPipe implements PipeTransform { + transform(product: Product, _args?: []): string { + let logoUrl = product.logoUrl; + if (logoUrl === undefined || logoUrl === '') { + logoUrl = `/assets/images/misc/axonivy-logo-round.png`; + } + return logoUrl; + } +} diff --git a/marketplace-ui/src/app/shared/pipes/multilingualism.pipe.ts b/marketplace-ui/src/app/shared/pipes/multilingualism.pipe.ts new file mode 100644 index 000000000..154c344fd --- /dev/null +++ b/marketplace-ui/src/app/shared/pipes/multilingualism.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Language } from '../enums/language.enum'; +import { DisplayValue } from '../models/display-value.model'; + +@Pipe({ + standalone: true, + name: 'multilingualism' +}) +export class MultilingualismPipe implements PipeTransform { + transform(value: DisplayValue, language: Language, _args?: []): string { + let displayValue = ''; + if (value !== undefined) { + displayValue = value[language]; + if (displayValue === undefined || displayValue === '') { + displayValue = value[Language.EN]; + } + } + + return displayValue; + } +} diff --git a/marketplace-ui/src/assets/.gitkeep b/marketplace-ui/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/marketplace-ui/src/assets/fonts/inter.ttf b/marketplace-ui/src/assets/fonts/inter.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e72470871b8fc198da424b1e17ed729c202829cf GIT binary patch literal 804612 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

4*9^KT7^n>*M>|wJB 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%0DCo0&`A-%p#lSZ_a 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

r4&=ywuOFzp>FIx^> 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 Uh@3pmyhdg$M{-oyJW7cF3l>xKP5=M^ literal 0 HcmV?d00001 diff --git a/marketplace-ui/src/assets/i18n/de.yaml b/marketplace-ui/src/assets/i18n/de.yaml new file mode 100644 index 000000000..d85f73589 --- /dev/null +++ b/marketplace-ui/src/assets/i18n/de.yaml @@ -0,0 +1,55 @@ +common: + branch: Axon Ivy Marktplatz + language: + english: Englisch + german: Deutsch + nav: + news: News + doc: Dokumentation + tutorial: Tutorial + community: Community + team: Team + market: Marktplatz + introduction: + about: Durch die vielfältigen Angebote des Axon Ivy Marktplatzes wird die Prozessautomatisierung umfassend erweitert. + contribute: Mit leistungsstarken Konnektoren, beispielsweise zur Einbindung von ChatGPT, MS Office oder den gängigen Datenbanken wird die Prozessautomatisierung komfortabel einfach. + link: Werde Teil der Community und + baue deinen eigenen Konnektor. + filter: + label: Filter + value: + allTypes: Alle + connector: Konnektoren + util: Utilities + demos: Demos + solution: Lösungen + sort: + label: Sortierung + value: + popularity: Beliebtheit + alphabetically: Alphabetisch + recent: Datum der Veröffentlichung + search: + placeholder: Suche + nothingFound: Nichts gefunden + header: + download: Download + footer: + newVersionsInfo: Neue Version verfügbar für Linux,
Windows und MacOS Betriebssysteme. + downloadLatestLTSVersion: Herunterladen V10.0.18 + downloadLatestDevVersion: Herunterladen V11.2.1 + ivyCompanyInfo: © 2023 Axon Ivy Inc + privacyPolicy: Datenschutzrichtlinie + termsOfService: Nutzungsbedingungen + product: + detail: + install: + buttonLabel: Jetzt installieren + download: + buttonLabel: Download + hideDevVersions: Entwicklungsversionen ausblenden + showDevVersions: Entwicklungsversionen anzeigen + versionSelector: + label: Zielplattform + artifactSelector: + label: Artefakt \ No newline at end of file diff --git a/marketplace-ui/src/assets/i18n/en.yaml b/marketplace-ui/src/assets/i18n/en.yaml new file mode 100644 index 000000000..db1da4ff6 --- /dev/null +++ b/marketplace-ui/src/assets/i18n/en.yaml @@ -0,0 +1,59 @@ +common: + branch: Axon Ivy Market + language: + english: English + french: Français + german: Deutsch + nav: + news: News + doc: Doc + tutorial: Tutorial + community: Community + team: Team + market: Market + introduction: + about: The Axon Ivy market offers a unique experience to accelerate process automation. + contribute: + From standard connectors of leading providers to, ready-to-use process + models, business solutions and many utilities such as document + generation and workflow user interfaces. + link: Contribute to the community and + build your own connector! + filter: + label: Filter + value: + allTypes: All Types + connector: Connectors + util: Utilities + demos: Demos + solution: Solutions + sort: + label: Sort by + value: + popularity: Popularity + alphabetically: Alphabetically + recent: Recent + search: + placeholder: Search a keyword + nothingFound: Nothing found + header: + download: Download + footer: + newVersionsInfo: New version release available for Linux,
Window and MacOS operating systems. + downloadLatestLTSVersion: Download V10.0.18 + downloadLatestDevVersion: Download V11.2.1 + ivyCompanyInfo: © 2023 Axon Ivy Inc + privacyPolicy: Privacy Policy + termsOfService: Terms of Service + product: + detail: + install: + buttonLabel: Install Now + download: + buttonLabel: Download + hideDevVersions: Hide development versions + showDevVersions: Show development versions + versionSelector: + label: Choose target platform + artifactSelector: + label: Choose artifact diff --git a/marketplace-ui/src/assets/images/misc/axonivy-logo-black.svg b/marketplace-ui/src/assets/images/misc/axonivy-logo-black.svg new file mode 100644 index 000000000..8d3f5d064 --- /dev/null +++ b/marketplace-ui/src/assets/images/misc/axonivy-logo-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/marketplace-ui/src/assets/images/misc/axonivy-logo-round.png b/marketplace-ui/src/assets/images/misc/axonivy-logo-round.png new file mode 100644 index 0000000000000000000000000000000000000000..7b139c2e685daed541df668990c1e0053cbf5100 GIT binary patch literal 2418 zcmb7GdpK0<8h_WC+cLv8lSGoH3n_J)MlMm!$h{(jkQm)BL`4~;S;eH3hJCtUT(;Ut zYQ|0xV(pHd%PuJzv&&`D9l2DBG-fZS=RD8pIe(o$*7LsKd->k?dw;+6t##PP+g*=o z!UO<4PY)MA0EAQ#z^J77yhrq&6x5AQUQPgoGS;^U8oB1K_3-loNU#9FlL5YxCj2cx z91oyV1mJKQz$oT?sqX^Pfv#EV?gH8r+5Jj#r4!>3yc2+BF!~^H_PjA^WW;)UxiVfe zbum*W)r{Q=fUWm*aq^FU`o4AHj*cZ`Y3dtPb9rixjz?`xe9qLs?7g9pq9Tfz;-6ul z9y|TxVm{w*`b+Q%iz6-M4EQ(&v&mNRbZ9q~?TC}UZr4fMQ`DoVw=KVQtc z+LfE{Ea=sXw(d!Z$f=hV1`nGZf}sZ#3KXd zTkA9@Od2~AxSrRuzUpuB2w~n0YF%!+pLg<`caPz*Cz@1Y)bJBcIj7;VEMa!~w9b85 zoV<8nW3wkgVZAK>vaZVqJMK8DCgf-AokJG{>v%}M+|=X^_dXF;>AvldN%vc?q=aa0 z_-md16nt0ifC%>=0vwda|ndG_O%HviH=wS zbcbucWf&t0R3W}uZ7`RN;!1A1dmjaRB;GOea@9jBp(7mHkoAa`;|t3uiao{6$HpNs zsA6w%^UtFSuPmn0AL;8K*YL7+UvBE~HAB#c?&aO$X|8Y-&YM&(zlh-+yt}Gm{tQ%2 zQCFW6_HOmt9v+S1`glj(AlYja*lzsX?lT1z!3XQ%H^(tt4{x->J7vk`lff2J>95l- zq3rO9@=&BHm^PLvj64yTS@BVS-%{^GPy+UqXkIZhXeQVq_O`L+zLumi*ljX$_$MR= zNl*N6j_*Jvs6Tp=!wPIdXo% zk3fn*r0o1gMs26W1T)e+Mk|dBC@!IDJv>34V#pMh!*L@?ZS{UArzmcBSURFq^+dco zHJ8VQURZYBzCQ2Xc0U@|4R)Z269tC(mXjkx$~=ar8+HGW8m4U|ndpJw-8ry!gC zUa3=AWr+5n#K^&ouXK=jgj@Ms{9xi_lH1FoO!XCVgDx-~?viadUCM5nh6j$p7bMBOGA0x7=(Ljq*%WE_A{D zqtv)7Fj&^OXa^3F6vey2(A}>&BsG6K_Rz*!2D2y)J*RL}jNn5VR^RME0?{0>%@hfu zi^lvuq#W|O=mX`5XwnbJaPy%_d^giQwvFs6(~6u8w*HQsrVW7lfD64*pEGQ7S5rj> zxXQw>3?nR{u?9^)>k8u#dpD&f?!~#8(2THOER4H~J54=AP6z)rpEo{2bxzovK_@JJ zyfz9D-Vu8k$H8Yq$4IlS8=d6Frr$E}PbP6#xBfMoq;#p4T{Q_+Pzj3XcnRZuH88N z_IRp@f?p|wWe>Q(0@JiON$XHly)o{fS2WKUtT5qS8teIg+A3P~pga69kTR~fB(h;K3iftMMd4&r)v!>Bw1rtvJ%6pN6I!zL-bw3l2Q(~ti*JWeRE|QXy37Q&8nP2FSLKH(e9kkB6V-I zIx1?Bx9xo?eO4{XF{&*MewX5Ud9`*CmJ}T^chKyXCh4_w*S!QSJM&*NXWdOSwzGMz zW!z&-DQMh!X9oEXVjk-n6uT}WcD=*e9qUN}d>)@adoFMGT)scg-hpr9Fpp=&<2mqn n({rBCe@KXmUblIJ_e-hIXw literal 0 HcmV?d00001 diff --git a/marketplace-ui/src/assets/images/misc/axonivy-logo.svg b/marketplace-ui/src/assets/images/misc/axonivy-logo.svg new file mode 100644 index 000000000..037c3f930 --- /dev/null +++ b/marketplace-ui/src/assets/images/misc/axonivy-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/marketplace-ui/src/assets/scss/custom-style.scss b/marketplace-ui/src/assets/scss/custom-style.scss new file mode 100644 index 000000000..6dea467fa --- /dev/null +++ b/marketplace-ui/src/assets/scss/custom-style.scss @@ -0,0 +1,243 @@ +// Required +@import '../../../node_modules/bootstrap/scss/functions'; + +// Required +@import '../../../node_modules/bootstrap/scss/variables'; +@import '../../../node_modules/bootstrap/scss/mixins'; +@font-face { + font-family: Inter; + src: url(../fonts/inter.ttf) format('truetype'); +} + +// Light +$ivyPrimaryColorLight: #007095; +$ivyPrimaryColorHoverLight: #00445c; +$ivySecondaryColorLight: #f8f8f8; +$ivySecondaryButtonHoverLight: #ebebeb; +$ivyPrimaryTextColorLight: #1b1b1b; +$ivySecondaryTextLight: #4a4a4a; +$ivyNormalTextColorLight: #fafafa; + +// Dark +$ivyPrimaryColorDark: 44, 44, 44; +$ivySecondaryColorDarkRGB: 56, 56, 56; +$ivyBodyBackgroundDark: #2c2c2c; +$ivySecondaryColorDark: #383838; +$ivySecondaryHoverDark: #202020; +$ivyPrimaryButtonHoverDark: #ececec; +$ivySecondaryTextDark: #a3a3a3; + +h1 { + font-weight: 600; + font-size: 72px; +} + +h3 { + font-weight: 600; + font-size: 22px; +} + +h4 { + font-weight: 400; + font-size: 18px; +} + +p { + font-weight: 400; + font-size: 14px; +} + +[data-bs-theme='light'] { + --ivy-primary-bg: #{$ivyPrimaryColorLight}; + --ivy-secondary-bg: #{$ivySecondaryColorLight}; + --ivy-active-color: #{$ivyPrimaryColorLight}; + --ivy-link-corlor: #{$ivyPrimaryColorLight}; + --ivy-text-normal-color: #{$ivyNormalTextColorLight}; + --ivy-text-primary-color: $ivyPrimaryTextColorLight; + --ivy-text-secondary-color: $ivySecondaryTextLight; + --ivy-border-color: #{$ivySecondaryButtonHoverLight}; + --header-border-color: #ebebeb; + --ivy-secondary-border-color: #e7e7e7; + + .bg-primary { + background-color: #{$ivyPrimaryColorLight} !important; + } + + .bg-secondary { + background-color: #{$ivySecondaryColorLight} !important; + } + + .card { + background-color: #{$ivySecondaryColorLight}; + border: 1px solid #{$ivySecondaryTextLight}; + } + + .btn { + border-radius: 0.5rem; + } + + .btn-primary { + background-color: #{$ivyPrimaryColorLight}; + color: #{$ivyNormalTextColorLight} !important; + } + + .btn-primary:hover { + background-color: #{$ivyPrimaryColorHoverLight}; + } + + .btn-secondary { + background-color: #{$ivySecondaryColorLight}; + color: #{$ivyPrimaryTextColorLight}; + } + + .btn-secondary:hover { + background-color: #{$ivySecondaryButtonHoverLight}; + } + + .text-primary { + color: #{$ivyPrimaryTextColorLight} !important; + } + + .text-secondary { + color: #{$ivySecondaryTextLight} !important; + } + + .text-dark { + color: #{$ivyPrimaryTextColorLight} !important; + } + + .border-dark { + border-color: #{$ivyPrimaryTextColorLight}; + } + + .border-primary { + border-color: #{$ivyPrimaryTextColorLight} !important; + } +} + +.spinner-border { + width: 100px; + height: 100px; +} + +[data-bs-theme='dark'] { + --bs-body-bg: #{$ivyBodyBackgroundDark}; + --bs-primary-rgb: #{$ivyPrimaryColorDark}; + --bs-secondary-rgb: #{$ivySecondaryColorDarkRGB}; + --bs-tertiary-bg: #{$ivySecondaryColorDarkRGB}; + --ivy-link-corlor: #{$ivyPrimaryColorLight}; + --ivy-primary-bg: #{$ivyNormalTextColorLight}; + --ivy-secondary-bg: #{$ivySecondaryColorDark}; + --ivy-active-color: #{$white}; + + --ivy-text-normal-color: #{$ivyBodyBackgroundDark}; + --ivy-text-secondary-color: #{$ivySecondaryTextDark}; + + --ivy-border-color: #{$ivySecondaryTextLight}; + --header-border-color: #{$ivySecondaryTextLight}; + --ivy-secondary-border-color: #4f4e4e; + + a { + color: #{$white}; + } + + .bg-light { + background-color: #{$ivyNormalTextColorLight} !important; + } + + .btn { + border-radius: 0.5rem; + } + + .btn-primary { + background-color: #{$ivyNormalTextColorLight}; + color: #{$ivyBodyBackgroundDark} !important; + } + + .btn-primary:hover { + background-color: #{$ivyPrimaryButtonHoverDark}; + } + + .btn-secondary { + background-color: #{$ivySecondaryColorDark}; + border: 0.5px solid #{$ivySecondaryTextLight} !important; + } + + .btn-secondary:hover { + background-color: #{$ivySecondaryHoverDark}; + } + + .text-primary { + color: #{$white} !important; + } + + .text-secondary { + color: #{$ivySecondaryTextDark} !important; + } + + .text-dark { + color: #{$ivyBodyBackgroundDark} !important; + } + + .border-primary { + border-color: #{$white} !important; + } + + .border-light { + border-color: #{$white} !important; + } +} + +*:focus { + box-shadow: none !important; + outline: none !important; +} + +.card { + height: 250px; + padding: 20px; + margin: 0 32px 8px 0; + gap: 20px; + border-radius: 15px; +} + +.text-light { + color: #{$white} !important; +} + +.text-muted { + color: #858585 !important; +} + +@media (min-width: 1440px) { + .container { + min-width: 1120px; + } + + .col-xxl-3 { + flex: 0 0 auto; + width: 33.33333333% !important; + } +} + +@media (min-width: 1920px) { + .container { + min-width: 1505px; + } + .col-xxl-3 { + flex: 0 0 auto; + width: 25% !important; + } +} + +@media (min-width: 2560px) { + .container { + min-width: 1505px; + } +} + +@media (min-width: 3840px) { + .container { + min-width: 2160px; + } +} diff --git a/marketplace-ui/src/environments/environment.development.ts b/marketplace-ui/src/environments/environment.development.ts new file mode 100644 index 000000000..d77f76d7b --- /dev/null +++ b/marketplace-ui/src/environments/environment.development.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiUrl: 'http://localhost:9090/marketplace-service' +}; diff --git a/marketplace-ui/src/environments/environment.ts b/marketplace-ui/src/environments/environment.ts new file mode 100644 index 000000000..b0c3ad6e3 --- /dev/null +++ b/marketplace-ui/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiUrl: 'http://10.193.8.78:9090/marketplace-service' +}; diff --git a/marketplace-ui/src/favicon.ico b/marketplace-ui/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..46d5a7ce60748444ee7b92a9770bf281f75583b1 GIT binary patch literal 701 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`q0KcVYP7-hXC4kjGiz z5n0T@z%2~Ij105pNB{-dOFVsD*`IOoa7Zz1ni%KGz`(e{)5S3);_%iRXQwt92(W*s z=d@Hh+^8*jgHg2LdOGtlt_6(R8=CHN?J3;cw6}Tz!#vS5ay;1|nA2{kSPH66{v+4u z!w}x%nVq##{a^aql}mjmCC6;4ZCLlWDmHTc?=5_{cCOC+D`9(lr8c+1$(K^v3}25m zY`eHC{m~Z2t%l42+&?dGe!O_~QYOw%7fWoWbSLpWVo1?H-^%fyrtIVZ4j3N84)c#2}8Hu5r>mdKI;Vst E00XZ9hX4Qo literal 0 HcmV?d00001 diff --git a/marketplace-ui/src/index.html b/marketplace-ui/src/index.html new file mode 100644 index 000000000..21bb621d2 --- /dev/null +++ b/marketplace-ui/src/index.html @@ -0,0 +1,13 @@ + + + + + Axon Ivy Market + + + + + + + + diff --git a/marketplace-ui/src/main.ts b/marketplace-ui/src/main.ts new file mode 100644 index 000000000..3997d76eb --- /dev/null +++ b/marketplace-ui/src/main.ts @@ -0,0 +1,9 @@ +/// + +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig).catch(err => { + throw err; +}); diff --git a/marketplace-ui/src/styles.scss b/marketplace-ui/src/styles.scss new file mode 100644 index 000000000..27ca1cce6 --- /dev/null +++ b/marketplace-ui/src/styles.scss @@ -0,0 +1,9 @@ +@import 'bootstrap'; +@import 'bootstrap-icons'; +@import './assets/scss/custom-style.scss'; + +* { + margin: 0; + padding: 0; + font-family: 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif; +} diff --git a/marketplace-ui/tsconfig.app.json b/marketplace-ui/tsconfig.app.json new file mode 100644 index 000000000..ec15b4b2d --- /dev/null +++ b/marketplace-ui/tsconfig.app.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "node", + "@angular/localize" + ] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/marketplace-ui/tsconfig.json b/marketplace-ui/tsconfig.json new file mode 100644 index 000000000..f37b67ff0 --- /dev/null +++ b/marketplace-ui/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/marketplace-ui/tsconfig.spec.json b/marketplace-ui/tsconfig.spec.json new file mode 100644 index 000000000..c63b6982a --- /dev/null +++ b/marketplace-ui/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine", + "@angular/localize" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/src/main/resources/github.token b/src/main/resources/github.token deleted file mode 100644 index f0208d200..000000000 --- a/src/main/resources/github.token +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 479d74cc104ee34f56431308d1e13f1dab799b8a Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Fri, 5 Jul 2024 11:58:54 +0700 Subject: [PATCH 14/62] Remove CI --- .github/workflows/ci-build.yml | 50 ---------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/ci-build.yml diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml deleted file mode 100644 index 86a8ff9ed..000000000 --- a/.github/workflows/ci-build.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: CI Build -run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} - -on: - push: - workflow_dispatch: - -jobs: - build: - name: Executes Tests - runs-on: self-hosted - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - name: Tests with Maven - run: mvn clean install - analysis: - name: Sonarqube analysis - needs: build - runs-on: self-hosted - env: - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_PROJECT_KEY : ${{ secrets.SONAR_PROJECT_KEY }} - steps: - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - name: Run SonarQube Scanner - run: | - mvn -B verify sonar:sonar \ - -Dsonar.host.url=${{ env.SONAR_HOST_URL }} \ - -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} \ - -Dsonar.projectName="AxonIvy Market Service" \ - -Dsonar.token=${{ env.SONAR_TOKEN }} \ - - name: SonarQube Quality Gate check - id: sonarqube-quality-gate-check - uses: sonarsource/sonarqube-quality-gate-action@master - timeout-minutes: 5 - with: - scanMetadataReportFile: target/sonar/report-task.txt - args: -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} From d57f98e445233edd14376c6289a69e639e8e8254 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Fri, 5 Jul 2024 14:53:22 +0700 Subject: [PATCH 15/62] Fix actions build --- .github/workflows/service-ci-build.yml | 10 +++++----- .github/workflows/service-dev-build.yml | 8 ++++---- .github/workflows/ui-ci-build.yml | 2 +- .github/workflows/ui-dev-build.yml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/service-ci-build.yml b/.github/workflows/service-ci-build.yml index 86a8ff9ed..e384445d2 100644 --- a/.github/workflows/service-ci-build.yml +++ b/.github/workflows/service-ci-build.yml @@ -1,5 +1,5 @@ -name: CI Build -run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} +name: Service CI Build +run-name: Build Service on branch ${{github.ref_name}} triggered by ${{github.actor}} on: push: @@ -19,7 +19,7 @@ jobs: distribution: 'temurin' cache: maven - name: Tests with Maven - run: mvn clean install + run: mvn clean install --file ./marketplace-service/pom.xml analysis: name: Sonarqube analysis needs: build @@ -36,7 +36,7 @@ jobs: distribution: 'temurin' - name: Run SonarQube Scanner run: | - mvn -B verify sonar:sonar \ + mvn -B verify sonar:sonar --file ./marketplace-service/pom.xml \ -Dsonar.host.url=${{ env.SONAR_HOST_URL }} \ -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} \ -Dsonar.projectName="AxonIvy Market Service" \ @@ -46,5 +46,5 @@ jobs: uses: sonarsource/sonarqube-quality-gate-action@master timeout-minutes: 5 with: - scanMetadataReportFile: target/sonar/report-task.txt + scanMetadataReportFile: ./marketplace-service/target/sonar/report-task.txt args: -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} diff --git a/.github/workflows/service-dev-build.yml b/.github/workflows/service-dev-build.yml index 137e06a6c..716ebe4b5 100644 --- a/.github/workflows/service-dev-build.yml +++ b/.github/workflows/service-dev-build.yml @@ -1,4 +1,4 @@ -name: DEV Build +name: Service DEV Build run-name: Build and Deploy Marketplace-Service on branch ${{github.ref_name}} by ${{github.actor}} on: @@ -21,8 +21,8 @@ jobs: cache: maven - name: Update configuration env: - APP_PROPERTIES_FILE: 'src/main/resources/application.properties' - GITHUB_TOKEN_FILE: 'src/main/resources/github.token' + APP_PROPERTIES_FILE: './marketplace-service/src/main/resources/application.properties' + GITHUB_TOKEN_FILE: './marketplace-service/src/main/resources/github.token' MONGODB_HOST: ${{ secrets.MONGODB_HOST }} MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }} GH_TOKEN: ${{ secrets.GH_TOKEN }} @@ -31,7 +31,7 @@ jobs: sed -i "s/^spring.data.mongodb.database=.*$/spring.data.mongodb.database=$MONGODB_DATABASE/" $APP_PROPERTIES_FILE sed -i '1d;$d' $GITHUB_TOKEN_FILE && echo $GH_TOKEN > $GITHUB_TOKEN_FILE - name: Build with Maven - run: mvn clean package -DskipTests + run: mvn clean package -DskipTests --file ./marketplace-service/pom.xml - name: Prepare deployment directory run: mkdir -p deployment && cp target/*.war deployment/ - name: Copy WAR to Tomcat server diff --git a/.github/workflows/ui-ci-build.yml b/.github/workflows/ui-ci-build.yml index 8a437d1f4..8137daf8b 100644 --- a/.github/workflows/ui-ci-build.yml +++ b/.github/workflows/ui-ci-build.yml @@ -1,4 +1,4 @@ -name: CI Build +name: UI CI Build run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} on: diff --git a/.github/workflows/ui-dev-build.yml b/.github/workflows/ui-dev-build.yml index cb34d8665..506278838 100644 --- a/.github/workflows/ui-dev-build.yml +++ b/.github/workflows/ui-dev-build.yml @@ -1,4 +1,4 @@ -name: Dev Build +name: UI Dev Build run-name: Build and Deploy Marketplace-UI on branch ${{github.ref_name}} by ${{github.actor}} on: From 3c900c21529412011ea0fd022fbdc7519d7c9d93 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:02:06 +0700 Subject: [PATCH 16/62] Added token file (#26) Co-authored-by: Hoan Nguyen Co-authored-by: ivyTeam --- github.token | 1 + 1 file changed, 1 insertion(+) create mode 100644 github.token diff --git a/github.token b/github.token new file mode 100644 index 000000000..8433821ba --- /dev/null +++ b/github.token @@ -0,0 +1 @@ +ghp_replace-with-your-token \ No newline at end of file From 50234e9ff5296c35388f03435be3872d4d205e66 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:13:42 +0700 Subject: [PATCH 17/62] Add files via upload From d843d0caa84145757c2ca1bf2f602accaecff70a Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Fri, 5 Jul 2024 15:15:25 +0700 Subject: [PATCH 18/62] Added token file --- .../src/main/resources/github.token | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename github.token => marketplace-service/src/main/resources/github.token (100%) diff --git a/github.token b/marketplace-service/src/main/resources/github.token similarity index 100% rename from github.token rename to marketplace-service/src/main/resources/github.token From 7e4a1aa5b48f3bde6b5f82381960d4e0b02c2a17 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Fri, 5 Jul 2024 15:17:42 +0700 Subject: [PATCH 19/62] Fix target URL --- .github/workflows/service-dev-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/service-dev-build.yml b/.github/workflows/service-dev-build.yml index 716ebe4b5..e310a9378 100644 --- a/.github/workflows/service-dev-build.yml +++ b/.github/workflows/service-dev-build.yml @@ -33,7 +33,7 @@ jobs: - name: Build with Maven run: mvn clean package -DskipTests --file ./marketplace-service/pom.xml - name: Prepare deployment directory - run: mkdir -p deployment && cp target/*.war deployment/ + run: mkdir -p deployment && cp ./marketplace-service/target/*.war deployment/ - name: Copy WAR to Tomcat server run: sudo cp deployment/*.war /opt/tomcat/webapps/marketplace-service.war - name: Restart Tomcat server From 8dfcb3b5d7934adab85f599adfc0ac48bf81d053 Mon Sep 17 00:00:00 2001 From: Thuy Nguyen <145430420+nntthuy-axonivy@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:45:23 +0700 Subject: [PATCH 20/62] MARP-572 Detail pages for new market main content (#24) Introduce get contents from folder product of release tag --- .../ProductDetailModelAssembler.java | 72 ++ .../assembler/ProductModelAssembler.java | 5 +- .../market/constants/CommonConstants.java | 7 +- .../market/constants/MavenConstants.java | 23 +- .../market/constants/MetaConstants.java | 11 + .../NonStandardProductPackageConstants.java | 39 +- .../constants/ProductJsonConstants.java | 32 +- .../market/constants/ReadmeConstants.java | 12 + .../market/controller/ProductController.java | 4 +- .../controller/ProductDetailsController.java | 58 +- .../com/axonivy/market/entity/Product.java | 95 +- .../market/entity/ProductModuleContent.java | 25 + .../market/factory/ProductFactory.java | 152 +-- .../com/axonivy/market/github/model/Meta.java | 4 +- .../service/GHAxonIvyProductRepoService.java | 14 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 370 ++++-- .../market/github/util/GitHubUtils.java | 85 ++ .../market/model/ProductDetailModel.java | 38 + .../market/repository/ProductRepository.java | 2 + .../market/service/ProductService.java | 4 + .../service/impl/ProductServiceImpl.java | 168 ++- .../service/impl/VersionServiceImpl.java | 601 +++++----- .../axonivy/market/util/XmlReaderUtils.java | 58 + .../axonivy/market/utils/XmlReaderUtils.java | 59 - .../ProductDetailsControllerTest.java | 77 +- .../market/factory/ProductFactoryTest.java | 4 +- .../GHAxonIvyProductRepoServiceImplTest.java | 242 ---- .../GHAxonIvyProductRepoServiceImplTest.java | 375 ++++++ .../service/ProductServiceImplTest.java | 104 +- .../service/VersionServiceImplTest.java | 1036 ++++++++--------- .../axonivy/market/util/GitHubUtilsTest.java | 93 ++ .../{utils => util}/XmlReaderUtilsTest.java | 16 +- 32 files changed, 2308 insertions(+), 1577 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java delete mode 100644 marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java rename marketplace-service/src/test/java/com/axonivy/market/{utils => util}/XmlReaderUtilsTest.java (58%) diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java new file mode 100644 index 000000000..dbfa0be0b --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -0,0 +1,72 @@ +package com.axonivy.market.assembler; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import com.axonivy.market.controller.ProductDetailsController; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.model.ProductDetailModel; +import org.apache.commons.lang3.StringUtils; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ProductDetailModelAssembler extends RepresentationModelAssemblerSupport { + + private final ProductModelAssembler productModelAssembler; + + public ProductDetailModelAssembler(ProductModelAssembler productModelAssembler) { + super(ProductDetailsController.class, ProductDetailModel.class); + this.productModelAssembler = productModelAssembler; + } + + @Override + public ProductDetailModel toModel(Product product) { + return createModel(product, null); + } + + public ProductDetailModel toModel(Product product, String tag) { + return createModel(product, tag); + } + + private ProductDetailModel createModel(Product product, String tag) { + ResponseEntity selfLinkWithTag; + ProductDetailModel model = instantiateModel(product); + productModelAssembler.createResource(model, product); + if (StringUtils.isBlank(tag)) { + selfLinkWithTag = methodOn(ProductDetailsController.class).findProductDetails(product.getId()); + } else { + selfLinkWithTag = methodOn(ProductDetailsController.class).findProductDetailsByVersion(product.getId(), tag); + } + model.add(linkTo(selfLinkWithTag).withSelfRel()); + createDetailResource(model, product, tag); + return model; + } + + private void createDetailResource(ProductDetailModel model, Product product, String tag) { + model.setVendor(product.getVendor()); + model.setNewestReleaseVersion(product.getNewestReleaseVersion()); + model.setPlatformReview(product.getPlatformReview()); + model.setSourceUrl(product.getSourceUrl()); + model.setStatusBadgeUrl(product.getStatusBadgeUrl()); + model.setLanguage(product.getLanguage()); + model.setIndustry(product.getIndustry()); + model.setCompatibility(product.getCompatibility()); + model.setContactUs(product.getContactUs()); + model.setCost(product.getCost()); + + if (StringUtils.isBlank(tag) && StringUtils.isNotBlank(product.getNewestReleaseVersion())) { + tag = product.getNewestReleaseVersion(); + } + ProductModuleContent content = getProductModuleContentByTag(product.getProductModuleContents(), tag); + model.setProductModuleContent(content); + } + + private ProductModuleContent getProductModuleContentByTag(List contents, String tag) { + return contents.stream().filter(content -> StringUtils.equals(content.getTag(), tag)).findAny().orElse(null); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java index bd9c948cd..00b94a52f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -20,12 +20,11 @@ public ProductModelAssembler() { @Override public ProductModel toModel(Product product) { ProductModel resource = new ProductModel(); - resource.add(linkTo(methodOn(ProductDetailsController.class).findProduct(product.getId(), product.getType())) - .withSelfRel()); + resource.add(linkTo(methodOn(ProductDetailsController.class).findProductDetails(product.getId())).withSelfRel()); return createResource(resource, product); } - private ProductModel createResource(ProductModel model, Product product) { + public ProductModel createResource(ProductModel model, Product product) { model.setId(product.getId()); model.setNames(product.getNames()); model.setShortDescriptions(product.getShortDescriptions()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java index d0e28028e..5f6eea10d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java @@ -7,8 +7,11 @@ public class CommonConstants { public static final int INITIAL_PAGE = 1; public static final int INITIAL_PAGE_SIZE = 10; - public static final String SLASH = "/"; public static final String REQUESTED_BY = "X-Requested-By"; - public static final String META_FILE = "meta.json"; public static final String LOGO_FILE = "logo.png"; + public static final String SLASH = "/"; + public static final String DOT_SEPARATOR = "."; + public static final String PLUS = "+"; + public static final String DASH_SEPARATOR = "-"; + public static final String SPACE_SEPARATOR = " "; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java index 992a4289b..4ba05471d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java @@ -1,19 +1,14 @@ package com.axonivy.market.constants; public class MavenConstants { - private MavenConstants() { - } + private MavenConstants() {} - public static final String SNAPSHOT_RELEASE_POSTFIX = "-SNAPSHOT"; - public static final String SPRINT_RELEASE_POSTFIX = "-m"; - public static final String PRODUCT_ARTIFACT_POSTFIX = "-product"; - public static final String METADATA_URL_FORMAT = "%s/%s/%s/maven-metadata.xml"; - public static final String DEFAULT_IVY_MAVEN_BASE_URL = "https://maven.axonivy.com"; - public static final String DOT_SEPARATOR = "."; - public static final String GROUP_ID_URL_SEPARATOR = "/"; - public static final String ARTIFACT_ID_SEPARATOR = "-"; - public static final String ARTIFACT_NAME_SEPARATOR = " "; - public static final String ARTIFACT_DOWNLOAD_URL_FORMAT = "%s/%s/%s/%s/%s-%s.%s"; - public static final String ARTIFACT_NAME_FORMAT = "%s (%s)"; - public static final String VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE = "//versions/version/text()"; + public static final String SNAPSHOT_RELEASE_POSTFIX = "-SNAPSHOT"; + public static final String SPRINT_RELEASE_POSTFIX = "-m"; + public static final String PRODUCT_ARTIFACT_POSTFIX = "-product"; + public static final String METADATA_URL_FORMAT = "%s/%s/%s/maven-metadata.xml"; + public static final String DEFAULT_IVY_MAVEN_BASE_URL = "https://maven.axonivy.com"; + public static final String ARTIFACT_DOWNLOAD_URL_FORMAT = "%s/%s/%s/%s/%s-%s.%s"; + public static final String ARTIFACT_NAME_FORMAT = "%s (%s)"; + public static final String VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE = "//versions/version/text()"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java new file mode 100644 index 000000000..3b088f957 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java @@ -0,0 +1,11 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MetaConstants { + public static final String META_FILE = "meta.json"; + public static final String DEFAULT_VENDOR_NAME = "Axon Ivy AG"; + public static final String DEFAULT_VENDOR_URL = "https://www.axonivy.com"; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java index c2e14744e..133ff55ff 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java @@ -1,20 +1,27 @@ package com.axonivy.market.constants; public class NonStandardProductPackageConstants { - private NonStandardProductPackageConstants() { - } + private NonStandardProductPackageConstants() {} - public static final String PORTAL = "portal"; - public static final String MICROSOFT_365 = ""; // No meta.json - public static final String MICROSOFT_CALENDAR = "msgraph-calendar"; // no fix product json - public static final String MICROSOFT_MAIL = "msgraph-mail";// no fix product json - public static final String MICROSOFT_TEAMS = "msgraph-chat";// no fix product json - public static final String MICROSOFT_TODO = "msgraph-todo";// no fix product json - public static final String CONNECTIVITY_FEATURE = "connectivity-demo"; - public static final String EMPLOYEE_ONBOARDING = "employee-onboarding"; // Invalid meta.json - public static final String ERROR_HANDLING = "error-handling-demo"; - public static final String RULE_ENGINE_DEMOS = "rule-engine-demo"; - public static final String WORKFLOW_DEMO = "workflow-demo"; - public static final String HTML_DIALOG_DEMO = "html-dialog-demo"; - public static final String PROCESSING_VALVE_DEMO = "processing-valve-demo";// no product json -} \ No newline at end of file + public static final String PORTAL = "portal"; + public static final String MICROSOFT_REPO_NAME = "msgraph-connector"; + public static final String MICROSOFT_365 = "msgraph"; // No meta.json + public static final String MICROSOFT_CALENDAR = "msgraph-calendar"; // no fix product json + public static final String MICROSOFT_MAIL = "msgraph-mail";// no fix product json + public static final String MICROSOFT_TEAMS = "msgraph-chat";// no fix product json + public static final String MICROSOFT_TODO = "msgraph-todo";// no fix product json + public static final String CONNECTIVITY_FEATURE = "connectivity-demo"; + public static final String EMPLOYEE_ONBOARDING = "employee-onboarding"; // Invalid meta.json + public static final String ERROR_HANDLING = "error-handling-demo"; + public static final String RULE_ENGINE_DEMOS = "rule-engine-demo"; + public static final String WORKFLOW_DEMO = "workflow-demo"; + public static final String HTML_DIALOG_DEMO = "html-dialog-demo"; + public static final String PROCESSING_VALVE_DEMO = "processing-valve-demo";// no product json + public static final String OPENAI_CONNECTOR = "openai-connector"; + public static final String OPENAI_ASSISTANT = "openai-assistant"; + // Non standard image folder name + public static final String EXCEL_IMPORTER = "excel-importer"; + public static final String EXPRESS_IMPORTER = "express-importer"; + public static final String GRAPHQL_DEMO = "graphql-demo"; + public static final String DEEPL_CONNECTOR = "deepl-connector"; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java index 9ce62f956..96c6eb5e1 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java @@ -1,21 +1,21 @@ package com.axonivy.market.constants; public class ProductJsonConstants { + public static final String PRODUCT_JSON_FILE = "product.json"; + public static final String DATA = "data"; + public static final String REPOSITORIES = "repositories"; + public static final String URL = "url"; + public static final String ID = "id"; + public static final String PROJECTS = "projects"; + public static final String ARTIFACT_ID = "artifactId"; + public static final String GROUP_ID = "groupId"; + public static final String TYPE = "type"; + public static final String DEPENDENCIES = "dependencies"; + public static final String INSTALLERS = "installers"; + public static final String DEPENDENCY_SUFFIX = "-dependency"; + public static final String MAVEN_IMPORT_INSTALLER_ID = "maven-import"; + public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; + public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; - public static final String DATA = "data"; - public static final String REPOSITORIES = "repositories"; - public static final String URL = "url"; - public static final String ID = "id"; - public static final String PROJECTS = "projects"; - public static final String ARTIFACT_ID = "artifactId"; - public static final String GROUP_ID = "groupId"; - public static final String TYPE = "type"; - public static final String DEPENDENCIES = "dependencies"; - public static final String INSTALLERS = "installers"; - public static final String MAVEN_IMPORT_INSTALLER_ID = "maven-import"; - public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; - public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; - - private ProductJsonConstants() { - } + private ProductJsonConstants() {} } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java new file mode 100644 index 000000000..6d3024e9f --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java @@ -0,0 +1,12 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReadmeConstants { + public static final String IMAGES = "images"; + public static final String README_FILE = "README.md"; + public static final String DEMO_PART = "## Demo"; + public static final String SETUP_PART = "## Setup"; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index 55d0444ff..ce273cd33 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -75,8 +75,8 @@ public ResponseEntity syncProducts() { @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { - var emptyPagedModel = (PagedModel) pagedResourcesAssembler - .toEmptyModel(Page.empty(), ProductModel.class); + var emptyPagedModel = + (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), ProductModel.class); return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index c5536cd48..e83712718 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -8,6 +8,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; + import org.springframework.web.bind.annotation.PathVariable; import java.util.List; @@ -17,24 +21,36 @@ @RestController @RequestMapping(PRODUCT_DETAILS) public class ProductDetailsController { - private final VersionService service; - - public ProductDetailsController(VersionService service) { - this.service = service; - } - - @GetMapping("/{id}") - public ResponseEntity findProduct(@PathVariable("id") String key, - @RequestParam(name = "type", required = false) String type) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - @GetMapping("/{id}/versions") - public ResponseEntity> findProductVersionsById(@PathVariable("id") String id, - @RequestParam(name = "isShowDevVersion") boolean isShowDevVersion, - @RequestParam(name = "designerVersion", required = false) String designerVersion) { - List models = service.getArtifactsAndVersionToDisplay(id, isShowDevVersion, - designerVersion); - return new ResponseEntity<>(models, HttpStatus.OK); - } -} \ No newline at end of file + private final VersionService versionService; + private final ProductService productService; + private final ProductDetailModelAssembler detailModelAssembler; + + public ProductDetailsController(VersionService versionService, ProductService productService, + ProductDetailModelAssembler detailModelAssembler) { + this.versionService = versionService; + this.productService = productService; + this.detailModelAssembler = detailModelAssembler; + } + + @GetMapping("/{id}/{tag}") + public ResponseEntity findProductDetailsByVersion(@PathVariable("id") String id, + @PathVariable("tag") String tag) { + var productDetail = productService.fetchProductDetail(id); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity findProductDetails(@PathVariable("id") String id) { + var productDetail = productService.fetchProductDetail(id); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, null), HttpStatus.OK); + } + + @GetMapping("/{id}/versions") + public ResponseEntity> findProductVersionsById(@PathVariable("id") String id, + @RequestParam(name = "isShowDevVersion") boolean isShowDevVersion, + @RequestParam(name = "designerVersion", required = false) String designerVersion) { + List models = + versionService.getArtifactsAndVersionToDisplay(id, isShowDevVersion, designerVersion); + return new ResponseEntity<>(models, HttpStatus.OK); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index 9d89381f7..deb469a61 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -6,69 +6,68 @@ import java.util.Date; import java.util.List; -import com.axonivy.market.github.model.MavenArtifact; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import com.axonivy.market.github.model.MavenArtifact; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Document(PRODUCT) public class Product implements Serializable { - - private static final long serialVersionUID = -8770801877877277258L; - @Id - private String id; - private String marketDirectory; + private static final long serialVersionUID = -8770801877877277258L; + @Id + private String id; + private String marketDirectory; @JsonProperty - private MultilingualismValue names; - private String version; + private MultilingualismValue names; + private String version; @JsonProperty - private MultilingualismValue shortDescriptions; - private String logoUrl; - private Boolean listed; - private String type; - private List tags; - private String vendor; - private String vendorImage; - private String vendorUrl; - private String platformReview; - private String cost; - private String repositoryName; - private String sourceUrl; - private String statusBadgeUrl; - private String language; - private String industry; - private String compatibility; - private Boolean validate; - private Boolean contactUs; - private Integer installationCount; - private Date newestPublishedDate; - private String newestReleaseVersion; - private List artifacts; + private MultilingualismValue shortDescriptions; + private String logoUrl; + private Boolean listed; + private String type; + private List tags; + private String vendor; + private String vendorUrl; + private String platformReview; + private String cost; + private String repositoryName; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private String compatibility; + private Boolean validate; + private Boolean contactUs; + private Integer installationCount; + private Date newestPublishedDate; + private String newestReleaseVersion; + private List productModuleContents; + private List artifacts; - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); - } + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); + } -} \ No newline at end of file +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java new file mode 100644 index 000000000..d2b6d1145 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -0,0 +1,25 @@ +package com.axonivy.market.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProductModuleContent implements Serializable { + private static final long serialVersionUID = 1L; + private String tag; + private String description; + private String setup; + private String demo; + private Boolean isDependency; + private String name; + private String groupId; + private String artifactId; + private String type; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index 4cd0f6971..38e16a438 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -1,77 +1,81 @@ package com.axonivy.market.factory; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.*; import static org.apache.commons.lang3.StringUtils.EMPTY; +import com.axonivy.market.enums.Language; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.model.DisplayValue; +import com.axonivy.market.model.MultilingualismValue; +import org.apache.commons.lang3.BooleanUtils; + import java.io.IOException; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHContent; -import org.springframework.util.CollectionUtils; import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.Language; import com.axonivy.market.github.model.Meta; -import com.axonivy.market.github.util.GitHubUtils; -import com.axonivy.market.model.DisplayValue; -import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.util.CollectionUtils; @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ProductFactory { - private static final ObjectMapper MAPPER = new ObjectMapper(); - - public static Product mappingByGHContent(Product product, GHContent content) { - if (content == null) { - return product; - } - - var contentName = content.getName(); - if (StringUtils.endsWith(contentName, META_FILE)) { - mappingByMetaJSONFile(product, content); - } - if (StringUtils.endsWith(contentName, LOGO_FILE)) { - product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); - } - return product; - } - - public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { - Meta meta = null; - try { - meta = jsonDecode(ghContent); - } catch (Exception e) { - log.error("Mapping from Meta file by GHContent failed", e); - return product; - } - - product.setId(meta.getId()); - product.setNames(mappingMultilingualismValueByMetaJSONFile(meta.getNames())); - product.setMarketDirectory(extractParentDirectory(ghContent)); - product.setListed(meta.getListed()); - product.setType(meta.getType()); - product.setTags(meta.getTags()); - product.setVersion(meta.getVersion()); - product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); - product.setVendor(meta.getVendor()); - product.setVendorImage(meta.getVendorImage()); - product.setVendorUrl(meta.getVendorUrl()); - product.setPlatformReview(meta.getPlatformReview()); - product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); - product.setLanguage(meta.getLanguage()); - product.setIndustry(meta.getIndustry()); - extractSourceUrl(product, meta); - product.setArtifacts(meta.getMavenArtifacts()); - return product; - } + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static Product mappingByGHContent(Product product, GHContent content) { + if (content == null) { + return product; + } + + var contentName = content.getName(); + if (StringUtils.endsWith(contentName, META_FILE)) { + mappingByMetaJSONFile(product, content); + } + if (StringUtils.endsWith(contentName, LOGO_FILE)) { + product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); + } + return product; + } + + public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { + Meta meta = null; + try { + meta = jsonDecode(ghContent); + } catch (Exception e) { + log.error("Mapping from Meta file by GHContent failed", e); + return product; + } + + product.setId(meta.getId()); + product.setNames(mappingMultilingualismValueByMetaJSONFile(meta.getNames())); + product.setMarketDirectory(extractParentDirectory(ghContent)); + product.setListed(meta.getListed()); + product.setType(meta.getType()); + product.setTags(meta.getTags()); + product.setVersion(meta.getVersion()); + product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); + product.setVendor(StringUtils.isBlank(meta.getVendor()) ? DEFAULT_VENDOR_NAME : meta.getVendor()); + product.setVendorUrl(StringUtils.isBlank(meta.getVendorUrl()) ? DEFAULT_VENDOR_URL : meta.getVendorUrl()); + product.setPlatformReview(meta.getPlatformReview()); + product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); + product.setLanguage(meta.getLanguage()); + product.setIndustry(meta.getIndustry()); + product.setContactUs(BooleanUtils.isTrue(meta.getContactUs())); + product.setCost(StringUtils.isBlank(meta.getCost()) ? "Free" : StringUtils.capitalize(meta.getCost())); + product.setCompatibility(meta.getCompatibility()); + extractSourceUrl(product, meta); + product.setArtifacts(meta.getMavenArtifacts()); + return product; + } private static MultilingualismValue mappingMultilingualismValueByMetaJSONFile(List list) { MultilingualismValue value = new MultilingualismValue(); @@ -88,27 +92,27 @@ private static MultilingualismValue mappingMultilingualismValueByMetaJSONFile(Li return value; } - private static String extractParentDirectory(GHContent ghContent) { - var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); - return path.replace(ghContent.getName(), EMPTY); - } - - public static void extractSourceUrl(Product product, Meta meta) { - var sourceUrl = meta.getSourceUrl(); - if (StringUtils.isBlank(sourceUrl)) { - return; - } - String[] tokens = sourceUrl.split(SLASH); - var tokensLength = tokens.length; - var repositoryPath = sourceUrl; - if (tokensLength > 1) { - repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); - } - product.setRepositoryName(repositoryPath); - product.setSourceUrl(sourceUrl); - } - - private static Meta jsonDecode(GHContent ghContent) throws IOException { - return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); - } + private static String extractParentDirectory(GHContent ghContent) { + var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); + return path.replace(ghContent.getName(), EMPTY); + } + + public static void extractSourceUrl(Product product, Meta meta) { + var sourceUrl = meta.getSourceUrl(); + if (StringUtils.isBlank(sourceUrl)) { + return; + } + String[] tokens = sourceUrl.split(SLASH); + var tokensLength = tokens.length; + var repositoryPath = sourceUrl; + if (tokensLength > 1) { + repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); + } + product.setRepositoryName(repositoryPath); + product.setSourceUrl(sourceUrl); + } + + private static Meta jsonDecode(GHContent ghContent) throws IOException { + return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java index ee80857de..92e940487 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java @@ -31,8 +31,10 @@ public class Meta { private Boolean listed; private String version; private String vendor; - private String vendorImage; private String vendorUrl; private List tags; private List mavenArtifacts; + private String compatibility; + private Boolean contactUs; + private String cost; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index 3a9e85180..46afa0f0c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -1,7 +1,11 @@ package com.axonivy.market.github.service; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.github.model.MavenArtifact; + import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; import java.io.IOException; @@ -9,9 +13,11 @@ public interface GHAxonIvyProductRepoService { - GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion); + GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion); + + List getAllTagsFromRepoName(String repoName) throws IOException; - List getAllTagsFromRepoName(String repoName) throws IOException; + ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, String tag); - List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; -} \ No newline at end of file + List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index 6b6b8c38e..ba3d79740 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -1,127 +1,285 @@ package com.axonivy.market.github.service.impl; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import java.io.IOException; +import java.util.*; + +import com.axonivy.market.constants.*; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.github.util.GitHubUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import lombok.extern.log4j.Log4j2; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import com.axonivy.market.github.model.MavenArtifact; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; import org.springframework.stereotype.Service; import com.axonivy.market.github.service.GitHubService; +import org.springframework.util.CollectionUtils; -import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Log4j2 @Service public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { - private GHOrganization organization; - private final GitHubService gitHubService; - private String repoUrl; - private static final ObjectMapper objectMapper = new ObjectMapper(); - - public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { - this.gitHubService = gitHubService; - } - - @Override - public List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { - List artifacts = new ArrayList<>(); - InputStream contentStream = extractedContentStream(content); - if (Objects.isNull(contentStream)) { - return artifacts; - } - - JsonNode rootNode = objectMapper.readTree(contentStream); - JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); - - for (JsonNode mavenNode : installersNode) { - JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); - - // Not convert to artifact if id of node is not maven-import or maven-dependency - List installerIdsToDisplay = List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, - ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); - if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { - continue; - } - - // Extract repository URL - JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); - repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); - - // Process projects - if (dataNode.has(ProductJsonConstants.PROJECTS)) { - extractMavenArtifactFromJsonNode(dataNode, false, artifacts); - } - - // Process dependencies - if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { - extractMavenArtifactFromJsonNode(dataNode, true, artifacts); - } - } - return artifacts; - } - - public InputStream extractedContentStream(GHContent content) { - try { - return content.read(); - } catch (IOException | NullPointerException e) { - log.warn("Can not read the current content: {}", e.getMessage()); - return null; - } - } - - public void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, - List artifacts) { - String nodeName = ProductJsonConstants.PROJECTS; - if (isDependency) { - nodeName = ProductJsonConstants.DEPENDENCIES; - } - JsonNode dependenciesNode = dataNode.path(nodeName); - for (JsonNode dependencyNode : dependenciesNode) { - MavenArtifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); - artifacts.add(artifact); - } - } - - public MavenArtifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { - MavenArtifact artifact = new MavenArtifact(); - artifact.setRepoUrl(repoUrl); - artifact.setIsDependency(isDependency); - artifact.setGroupId(node.path(ProductJsonConstants.GROUP_ID).asText()); - artifact.setArtifactId(node.path(ProductJsonConstants.ARTIFACT_ID).asText()); - artifact.setType(node.path(ProductJsonConstants.TYPE).asText()); - artifact.setIsProductArtifact(true); - return artifact; - } - - @Override - public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { - try { - return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); - } catch (IOException e) { - log.error("Cannot Get Content From File Directory", e); - return null; - } - } - - public GHOrganization getOrganization() throws IOException { - if (organization == null) { - organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); - } - return organization; - } - - @Override - public List getAllTagsFromRepoName(String repoName) throws IOException { - return getOrganization().getRepository(repoName).listTags().toList(); - } + private GHOrganization organization; + private final GitHubService gitHubService; + private String repoUrl; + private static final ObjectMapper objectMapper = new ObjectMapper(); + public static final String DEMO_SETUP_TITLE = "(?i)## Demo|## Setup"; + public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; + public static final String README_IMAGE_FORMAT = "\\(([^)]*?%s[^)]*?)\\)"; + public static final String IMAGE_DOWNLOAD_URL_FORMAT = "(%s)"; + + public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { + this.gitHubService = gitHubService; + } + + @Override + public List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { + List artifacts = new ArrayList<>(); + InputStream contentStream = extractedContentStream(content); + if (Objects.isNull(contentStream)) { + return artifacts; + } + + JsonNode rootNode = objectMapper.readTree(contentStream); + JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); + + for (JsonNode mavenNode : installersNode) { + JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); + + // Not convert to artifact if id of node is not maven-import or maven-dependency + List installerIdsToDisplay = + List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); + if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { + continue; + } + + // Extract repository URL + JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); + repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); + + // Process projects + if (dataNode.has(ProductJsonConstants.PROJECTS)) { + extractMavenArtifactFromJsonNode(dataNode, false, artifacts); + } + + // Process dependencies + if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { + extractMavenArtifactFromJsonNode(dataNode, true, artifacts); + } + } + return artifacts; + } + + public InputStream extractedContentStream(GHContent content) { + try { + return content.read(); + } catch (IOException | NullPointerException e) { + log.warn("Can not read the current content: {}", e.getMessage()); + return null; + } + } + + public void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, List artifacts) { + String nodeName = ProductJsonConstants.PROJECTS; + if (isDependency) { + nodeName = ProductJsonConstants.DEPENDENCIES; + } + JsonNode dependenciesNode = dataNode.path(nodeName); + for (JsonNode dependencyNode : dependenciesNode) { + MavenArtifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); + artifacts.add(artifact); + } + } + + public MavenArtifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { + MavenArtifact artifact = new MavenArtifact(); + artifact.setRepoUrl(repoUrl); + artifact.setIsDependency(isDependency); + artifact.setGroupId(node.path(ProductJsonConstants.GROUP_ID).asText()); + artifact.setArtifactId(node.path(ProductJsonConstants.ARTIFACT_ID).asText()); + artifact.setType(node.path(ProductJsonConstants.TYPE).asText()); + artifact.setIsProductArtifact(true); + return artifact; + } + + @Override + public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { + try { + return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); + } catch (IOException e) { + log.error("Cannot Get Content From File Directory", e); + return null; + } + } + + public GHOrganization getOrganization() throws IOException { + if (organization == null) { + organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + } + return organization; + } + + @Override + public List getAllTagsFromRepoName(String repoName) throws IOException { + return getOrganization().getRepository(repoName).listTags().toList(); + } + + @Override + public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, + String tag) { + ProductModuleContent productModuleContent = new ProductModuleContent(); + try { + List contents = getProductFolderContents(product, ghRepository, tag); + productModuleContent.setTag(tag); + getDependencyContentsFromProductJson(productModuleContent, contents); + GHContent readmeFile = contents.stream().filter(GHContent::isFile) + .filter(content -> ReadmeConstants.README_FILE.equals(content.getName())).findFirst().orElse(null); + if (Objects.nonNull(readmeFile)) { + String readmeContents = new String(readmeFile.read().readAllBytes()); + if (hasImageDirectives(readmeContents)) { + readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); + } + getExtractedPartsOfReadme(productModuleContent, readmeContents); + } + } catch (Exception e) { + log.error("Cannot get product.json and README file's content {}", e); + return null; + } + return productModuleContent; + } + + private void getDependencyContentsFromProductJson(ProductModuleContent productModuleContent, List contents) + throws IOException { + GHContent productJsonFile = getProductJsonFile(contents); + if (Objects.nonNull(productJsonFile)) { + List artifacts = convertProductJsonToMavenProductInfo(productJsonFile); + MavenArtifact artifact = artifacts.stream().filter(MavenArtifact::getIsDependency).findFirst().orElse(null); + + if (Objects.nonNull(artifact)) { + productModuleContent.setIsDependency(Boolean.TRUE); + productModuleContent.setGroupId(artifact.getGroupId()); + productModuleContent.setArtifactId(artifact.getArtifactId()); + productModuleContent.setType(artifact.getType()); + productModuleContent.setName(artifact.getName()); + } + } + } + + private static GHContent getProductJsonFile(List contents) { + return contents.stream().filter(GHContent::isFile) + .filter(content -> ProductJsonConstants.PRODUCT_JSON_FILE.equals(content.getName())).findFirst().orElse(null); + } + + public String updateImagesWithDownloadUrl(Product product, List contents, String readmeContents) + throws IOException { + Map imageUrls = new HashMap<>(); + List productImages = contents.stream().filter(GHContent::isFile) + .filter(content -> content.getName().toLowerCase().matches(IMAGE_EXTENSION)).toList(); + if (!CollectionUtils.isEmpty(productImages)) { + for (GHContent productImage : productImages) { + imageUrls.put(productImage.getName(), productImage.getDownloadUrl()); + } + } else { + getImagesFromImageFolder(product, contents, imageUrls); + } + for (Map.Entry entry : imageUrls.entrySet()) { + String imageUrlPattern = String.format(README_IMAGE_FORMAT, Pattern.quote(entry.getKey())); + readmeContents = readmeContents.replaceAll(imageUrlPattern, String.format(IMAGE_DOWNLOAD_URL_FORMAT,entry.getValue())); + + } + return readmeContents; + } + + private void getImagesFromImageFolder(Product product, List contents, Map imageUrls) + throws IOException { + String imageFolderPath = GitHubUtils.getNonStandardImageFolder(product.getId()); + GHContent imageFolder = contents.stream().filter(GHContent::isDirectory) + .filter(content -> imageFolderPath.equals(content.getName())).findFirst().orElse(null); + if (Objects.nonNull(imageFolder)) { + for (GHContent imageContent : imageFolder.listDirectoryContent().toList()) { + imageUrls.put(imageContent.getName(), imageContent.getDownloadUrl()); + } + } + } + + // Cover some cases including when demo and setup parts switch positions or + // missing one of them + public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, String readmeContents) { + String[] parts = readmeContents.split(DEMO_SETUP_TITLE); + int demoIndex = readmeContents.indexOf(ReadmeConstants.DEMO_PART); + int setupIndex = readmeContents.indexOf(ReadmeConstants.SETUP_PART); + String description = Strings.EMPTY; + String setup = Strings.EMPTY; + String demo = Strings.EMPTY; + + if (parts.length > 0) { + description = removeFirstLine(parts[0]); + } + + if (demoIndex != -1 && setupIndex != -1) { + if (demoIndex < setupIndex) { + demo = parts[1]; + setup = parts[2]; + } else { + setup = parts[1]; + demo = parts[2]; + } + } else if (demoIndex != -1) { + demo = parts[1]; + } else if (setupIndex != -1) { + setup = parts[1]; + } + + productModuleContent.setDescription(description.trim()); + productModuleContent.setDemo(demo.trim()); + productModuleContent.setSetup(setup.trim()); + } + + private List getProductFolderContents(Product product, GHRepository ghRepository, String tag) + throws IOException { + String productFolderPath = ghRepository.getDirectoryContent(CommonConstants.SLASH, tag).stream() + .filter(GHContent::isDirectory).map(GHContent::getName) + .filter(content -> content.endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findFirst() + .orElse(null); + if (StringUtils.isBlank(productFolderPath) || hasChildConnector(ghRepository)) { + productFolderPath = GitHubUtils.getNonStandardProductFilePath(product.getId()); + } + + return ghRepository.getDirectoryContent(productFolderPath, tag); + } + + private boolean hasChildConnector(GHRepository ghRepository) { + return NonStandardProductPackageConstants.MICROSOFT_REPO_NAME.equals(ghRepository.getName()) + || NonStandardProductPackageConstants.OPENAI_CONNECTOR.equals(ghRepository.getName()); + } + + private boolean hasImageDirectives(String readmeContents) { + Pattern pattern = Pattern.compile(IMAGE_EXTENSION); + Matcher matcher = pattern.matcher(readmeContents); + return matcher.find(); + } + + private String removeFirstLine(String text) { + if (text.isBlank()) { + return Strings.EMPTY; + } + int index = text.indexOf(StringUtils.LF); + return index != -1 ? text.substring(index + 1).trim() : Strings.EMPTY; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java index 8d66b4649..4ccd7f1d4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -1,8 +1,13 @@ package com.axonivy.market.github.util; import java.io.IOException; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.NonStandardProductPackageConstants; +import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.PagedIterable; @@ -15,6 +20,9 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubUtils { + private static String pathToProductFolderFromTagContent; + private static String pathToImageFolder; + public static long getGHCommitDate(GHCommit commit) { long commitTime = 0l; if (commit != null) { @@ -46,4 +54,81 @@ public static List mapPagedIteratorToList(PagedIterable paged) { } return List.of(); } + + public static String convertArtifactIdToName(String artifactId) { + if (StringUtils.isBlank(artifactId)) { + return StringUtils.EMPTY; + } + return Arrays.stream(artifactId.split(CommonConstants.DASH_SEPARATOR)) + .map(part -> part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase()) + .collect(Collectors.joining(CommonConstants.SPACE_SEPARATOR)); + } + + public static String getNonStandardProductFilePath(String productId) { + switch (productId) { + case NonStandardProductPackageConstants.PORTAL: + pathToProductFolderFromTagContent = "AxonIvyPortal/portal-product"; + break; + case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: + pathToProductFolderFromTagContent = "connectivity/connectivity-demos-product"; + break; + case NonStandardProductPackageConstants.ERROR_HANDLING: + pathToProductFolderFromTagContent = "error-handling/error-handling-demos-product"; + break; + case NonStandardProductPackageConstants.WORKFLOW_DEMO: + pathToProductFolderFromTagContent = "workflow/workflow-demos-product"; + break; + case NonStandardProductPackageConstants.MICROSOFT_365: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-connector"; + break; + case NonStandardProductPackageConstants.MICROSOFT_CALENDAR: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-calendar"; + break; + case NonStandardProductPackageConstants.MICROSOFT_TEAMS: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-chat"; + break; + case NonStandardProductPackageConstants.MICROSOFT_MAIL: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-mail"; + break; + case NonStandardProductPackageConstants.MICROSOFT_TODO: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-todo"; + break; + case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: + pathToProductFolderFromTagContent = "html-dialog/html-dialog-demos-product"; + break; + case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: + pathToProductFolderFromTagContent = "rule-engine/rule-engine-demos-product"; + break; + case NonStandardProductPackageConstants.OPENAI_CONNECTOR: + pathToProductFolderFromTagContent = "openai-connector-product"; + break; + case NonStandardProductPackageConstants.OPENAI_ASSISTANT: + pathToProductFolderFromTagContent = "openai-assistant-product"; + break; + default: + break; + } + return pathToProductFolderFromTagContent; + } + + public static String getNonStandardImageFolder(String productId) { + switch (productId) { + case NonStandardProductPackageConstants.EXCEL_IMPORTER: + pathToImageFolder = "doc"; + break; + case NonStandardProductPackageConstants.EXPRESS_IMPORTER, NonStandardProductPackageConstants.DEEPL_CONNECTOR: + pathToImageFolder = "img"; + break; + case NonStandardProductPackageConstants.GRAPHQL_DEMO: + pathToImageFolder = "assets"; + break; + case NonStandardProductPackageConstants.OPENAI_ASSISTANT: + pathToImageFolder = "docs"; + break; + default: + pathToImageFolder = "images"; + break; + } + return pathToImageFolder; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java new file mode 100644 index 000000000..2943ccc79 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -0,0 +1,38 @@ +package com.axonivy.market.model; + +import com.axonivy.market.entity.ProductModuleContent; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@NoArgsConstructor +public class ProductDetailModel extends ProductModel { + private String vendor; + private String platformReview; + private String newestReleaseVersion; + private String cost; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private String compatibility; + private Boolean contactUs; + private ProductModuleContent productModuleContent; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(getId()).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(getId(), ((ProductDetailModel) obj).getId()).isEquals(); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index 25ff5e794..7fabd79bc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -17,6 +17,8 @@ public interface ProductRepository extends MongoRepository { Product findByLogoUrl(String logoUrl); + Product findByIdAndType(String id, String type); + Optional findById(String productId); @Query("{'marketDirectory': {$regex : ?0, $options: 'i'}}") diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index 5e35368de..b90604d42 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -9,4 +9,8 @@ public interface ProductService { Page findProducts(String type, String keyword, String language, Pageable pageable); boolean syncLatestDataFromMarketRepo(); + + Product fetchProductDetail(String id); + + String getCompatibilityFromOldestTag(String oldestTag); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 70f7a3a16..80bfe0645 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -5,13 +5,18 @@ import java.io.IOException; import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.entity.ProductModuleContent; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; @@ -26,16 +31,15 @@ import org.springframework.util.CollectionUtils; import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.Product; import com.axonivy.market.enums.FileType; import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; import com.axonivy.market.factory.ProductFactory; import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.enums.TypeOption; import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ProductService; @@ -48,16 +52,21 @@ public class ProductServiceImpl implements ProductService { private final ProductRepository productRepository; private final GHAxonIvyMarketRepoService axonIvyMarketRepoService; + private final GHAxonIvyProductRepoService axonIvyProductRepoService; private final GitHubRepoMetaRepository gitHubRepoMetaRepository; private final GitHubService gitHubService; private GHCommit lastGHCommit; private GitHubRepoMeta marketRepoMeta; + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, - GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService) { + GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, + GitHubService gitHubService) { this.productRepository = productRepository; this.axonIvyMarketRepoService = axonIvyMarketRepoService; + this.axonIvyProductRepoService = axonIvyProductRepoService; this.gitHubRepoMetaRepository = gitHubRepoMetaRepository; this.gitHubService = gitHubService; } @@ -68,22 +77,22 @@ public Page findProducts(String type, String keyword, String language, final var searchPageable = refinePagination(language, pageable); Page result = Page.empty(); switch (typeOption) { - case ALL: - if (StringUtils.isBlank(keyword)) { - result = productRepository.findAll(searchPageable); - } else { - result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); - } - break; - case CONNECTORS, UTILITIES, SOLUTIONS: - if (StringUtils.isBlank(keyword)) { - result = productRepository.findByType(typeOption.getCode(), searchPageable); - } else { - result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); - } - break; - default: - break; + case ALL: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findAll(searchPageable); + } else { + result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); + } + break; + case CONNECTORS, UTILITIES, SOLUTIONS: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findByType(typeOption.getCode(), searchPageable); + } else { + result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); + } + break; + default: + break; } return result; } @@ -107,8 +116,8 @@ private void syncRepoMetaDataStatus() { if (lastGHCommit == null) { return; } - String repoURL = Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath) - .orElse(EMPTY); + String repoURL = + Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath).orElse(EMPTY); marketRepoMeta.setRepoURL(repoURL); marketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); marketRepoMeta.setLastSHA1(lastGHCommit.getSHA1()); @@ -144,7 +153,7 @@ private void updateLatestChangeToProductsFromGithubRepo() { } ProductFactory.mappingByGHContent(product, fileContent); - updateLatestReleaseDateForProduct(product); + updateProductFromReleaseTags(product); if (FileType.META == file.getType()) { modifyProductByMetaContent(file, product); } else { @@ -157,34 +166,34 @@ private void updateLatestChangeToProductsFromGithubRepo() { private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { Product result = null; switch (file.getStatus()) { - case MODIFIED, ADDED: - result = productRepository.findByMarketDirectoryRegex(parentPath); - if (result != null) { - result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); - productRepository.save(result); - } - break; - case REMOVED: - result = productRepository.findByLogoUrl(product.getLogoUrl()); - if (result != null) { - productRepository.deleteById(result.getId()); - } - break; - default: - break; + case MODIFIED, ADDED: + result = productRepository.findByMarketDirectoryRegex(parentPath); + if (result != null) { + result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); + productRepository.save(result); + } + break; + case REMOVED: + result = productRepository.findByLogoUrl(product.getLogoUrl()); + if (result != null) { + productRepository.deleteById(result.getId()); + } + break; + default: + break; } } private void modifyProductByMetaContent(GitHubFile file, Product product) { switch (file.getStatus()) { - case MODIFIED, ADDED: - productRepository.save(product); - break; - case REMOVED: - productRepository.deleteById(product.getId()); - break; - default: - break; + case MODIFIED, ADDED: + productRepository.save(product); + break; + case REMOVED: + productRepository.deleteById(product.getId()); + break; + default: + break; } } @@ -225,7 +234,7 @@ private Page syncProductsFromGitHubRepo() { Product product = new Product(); for (var content : ghContentEntity.getValue()) { ProductFactory.mappingByGHContent(product, content); - updateLatestReleaseDateForProduct(product); + updateProductFromReleaseTags(product); } products.add(product); }); @@ -235,17 +244,64 @@ private Page syncProductsFromGitHubRepo() { return new PageImpl<>(products); } - private void updateLatestReleaseDateForProduct(Product product) { + private void updateProductFromReleaseTags(Product product) { if (StringUtils.isBlank(product.getRepositoryName())) { return; } try { GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); - GHTag lastTag = CollectionUtils.firstElement(productRepo.listTags().toList()); - product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); - product.setNewestReleaseVersion(lastTag.getName()); + List tags = productRepo.listTags().toList(); + GHTag lastTag = CollectionUtils.firstElement(tags); + if (lastTag != null) { + product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); + product.setNewestReleaseVersion(lastTag.getName()); + } + + String oldestTag = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)).distinct() + .sorted(Comparator.reverseOrder()).reduce((tag1, tag2) -> tag2).orElse(null); + if (oldestTag != null && StringUtils.isBlank(product.getCompatibility())) { + String compatibility = getCompatibilityFromOldestTag(oldestTag); + product.setCompatibility(compatibility); + } + + List> completableFutures = new ArrayList<>(); + ExecutorService service = Executors.newFixedThreadPool(10); + for (GHTag ghtag : tags) { + completableFutures.add(CompletableFuture.supplyAsync( + () -> axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()), + service)); + } + completableFutures.forEach(CompletableFuture::join); + List productModuleContents = completableFutures.stream().map(completableFuture -> { + try { + return completableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + log.error("Get readme and product json contents failed", e); + return null; + } + }).toList(); + product.setProductModuleContents(productModuleContents); } catch (Exception e) { log.error("Cannot find repository by path {} {}", product.getRepositoryName(), e); } } + + // Cover 3 cases after removing non-numeric characters (8, 11.1 and 10.0.2) + public String getCompatibilityFromOldestTag(String oldestTag) { + if (!oldestTag.contains(CommonConstants.DOT_SEPARATOR)) { + return oldestTag + ".0+"; + } + int firstDot = oldestTag.indexOf(CommonConstants.DOT_SEPARATOR); + int secondDot = oldestTag.indexOf(CommonConstants.DOT_SEPARATOR, firstDot + 1); + if (secondDot == -1) { + return oldestTag.concat(CommonConstants.PLUS); + } + return oldestTag.substring(0, secondDot).concat(CommonConstants.PLUS); + } + + @Override + public Product fetchProductDetail(String id) { + return productRepository.findById(id).orElse(null); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index 36988dcea..06d07123d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -1,5 +1,6 @@ package com.axonivy.market.service.impl; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.constants.NonStandardProductPackageConstants; @@ -9,13 +10,14 @@ import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.VersionService; import com.axonivy.market.comparator.ArchivedArtifactsComparator; import com.axonivy.market.comparator.LatestVersionComparator; -import com.axonivy.market.utils.XmlReaderUtils; +import com.axonivy.market.util.XmlReaderUtils; import lombok.Getter; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; @@ -26,7 +28,6 @@ import java.io.IOException; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; @Log4j2 @@ -34,317 +35,285 @@ @Getter public class VersionServiceImpl implements VersionService { - private final GHAxonIvyProductRepoService gitHubService; - private final MavenArtifactVersionRepository mavenArtifactVersionRepository; - private final ProductRepository productRepository; - @Getter - private String repoName; - private Map> archivedArtifactsMap; - private List artifactsFromMeta; - private MavenArtifactVersion proceedDataCache; - private MavenArtifact metaProductArtifact; - private final LatestVersionComparator latestVersionComparator = new LatestVersionComparator(); - @Getter - private String productJsonFilePath; - private String productId; - - public VersionServiceImpl(GHAxonIvyProductRepoService gitHubService, - MavenArtifactVersionRepository mavenArtifactVersionRepository, ProductRepository productRepository) { - this.gitHubService = gitHubService; - this.mavenArtifactVersionRepository = mavenArtifactVersionRepository; - this.productRepository = productRepository; - - } - - private void resetData() { - repoName = null; - archivedArtifactsMap = new HashMap<>(); - artifactsFromMeta = Collections.emptyList(); - proceedDataCache = null; - metaProductArtifact = null; - productJsonFilePath = null; - productId = null; - - } - - public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, - String designerVersion) { - List results = new ArrayList<>(); - resetData(); - - this.productId = productId; - artifactsFromMeta = getProductMetaArtifacts(productId); - List versionsToDisplay = getVersionsToDisplay(isShowDevVersion, designerVersion); - proceedDataCache = mavenArtifactVersionRepository.findById(productId) - .orElse(new MavenArtifactVersion(productId)); - metaProductArtifact = artifactsFromMeta.stream() - .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)) - .findAny().orElse(new MavenArtifact()); - - sanitizeMetaArtifactBeforeHandle(); - - boolean isNewVersionDetected = handleArtifactForVersionToDisplay(versionsToDisplay, results); - if (isNewVersionDetected) { - mavenArtifactVersionRepository.save(proceedDataCache); - } - return results; - } - - public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, - List result) { - boolean isNewVersionDetected = false; - for (String version : versionsToDisplay) { - List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); - List productArtifactModels = proceedDataCache.getProductArtifactWithVersionReleased() - .get(version); - if (productArtifactModels == null) { - isNewVersionDetected = true; - productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); - } - artifactsInVersion.addAll(productArtifactModels); - result.add(new MavenArtifactVersionModel(version, artifactsInVersion.stream().distinct().toList())); - } - return isNewVersionDetected; - } - - public List updateArtifactsInVersionWithProductArtifact(String version) { - List productArtifactModels = convertMavenArtifactsToModels(getProductJsonByVersion(version), - version); - proceedDataCache.getVersions().add(version); - proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); - return productArtifactModels; - } - - public List getProductMetaArtifacts(String productId) { - Product productInfo = productRepository.findById(productId).orElse(new Product()); - String fullRepoName = productInfo.getRepositoryName(); - if (StringUtils.isNotEmpty(fullRepoName)) { - repoName = getRepoNameFromMarketRepo(fullRepoName); - } - return Optional.ofNullable(productInfo.getArtifacts()).orElse(new ArrayList<>()); - } - - public void sanitizeMetaArtifactBeforeHandle() { - artifactsFromMeta.remove(metaProductArtifact); - artifactsFromMeta.forEach(artifact -> { - List archivedArtifacts = new ArrayList<>( - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).stream() - .sorted(new ArchivedArtifactsComparator()).toList()); - Collections.reverse(archivedArtifacts); - archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); - }); - } - - @Override - public List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion) { - List versions = getVersionsFromMavenArtifacts(); - Stream versionStream = versions.stream(); - if (BooleanUtils.isTrue(isShowDevVersion)) { - return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) - .sorted(new LatestVersionComparator()).toList(); - } - if (StringUtils.isNotBlank(designerVersion)) { - return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); - } - return versions.stream().filter(this::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); - } - - public List getVersionsFromMavenArtifacts() { - Set versions = new HashSet<>(); - for (MavenArtifact artifact : artifactsFromMeta) { - versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), - artifact.getArtifactId())); - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()) - .forEach(archivedArtifact -> versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), - archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()))); - } - List versionList = new ArrayList<>(versions); - versionList.sort(new LatestVersionComparator()); - return versionList; - } - - @Override - public List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactID) { - List versions = new ArrayList<>(); - String baseUrl = buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactID); - if (StringUtils.isNotBlank(baseUrl)) { - versions.addAll(XmlReaderUtils.readXMLFromUrl(baseUrl)); - } - return versions; - } - - @Override - public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactID) { - if (StringUtils.isAnyBlank(groupId, artifactID)) { - return StringUtils.EMPTY; - } - repoUrl = Optional.ofNullable(repoUrl).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - groupId = groupId.replace(MavenConstants.DOT_SEPARATOR, MavenConstants.GROUP_ID_URL_SEPARATOR); - return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); - } - - public String getBugfixVersion(String version) { - - if (isSnapshotVersion(version)) { - version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); - } else if (isSprintVersion(version)) { - version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; - } - String[] segments = version.split("\\."); - if (segments.length >= 3) { - segments[2] = segments[2].split(MavenConstants.ARTIFACT_ID_SEPARATOR)[0]; - return segments[0] + MavenConstants.DOT_SEPARATOR + segments[1] + MavenConstants.DOT_SEPARATOR - + segments[2]; - } - return version; - } - - public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { - if (isReleasedVersion(version)) { - return true; - } - String bugfixVersion; - if (isSnapshotVersion(version)) { - bugfixVersion = getBugfixVersion( - version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); - } else { - bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); - } - return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) - && getBugfixVersion(currentVersion).equals(bugfixVersion)); - } - - public boolean isSnapshotVersion(String version) { - return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); - } - - public boolean isSprintVersion(String version) { - return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); - } - - public boolean isReleasedVersion(String version) { - return !(isSprintVersion(version) || isSnapshotVersion(version)); - } - - public boolean isMatchWithDesignerVersion(String version, String designerVersion) { - return isReleasedVersion(version) && version.startsWith(designerVersion); - } - - public List getProductJsonByVersion(String version) { - List result = new ArrayList<>(); - String versionTag = buildProductJsonFilePath(version); - try { - GHContent productJsonContent = gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, - versionTag); - if (Objects.isNull(productJsonContent)) { - return result; - } - result = gitHubService.convertProductJsonToMavenProductInfo(productJsonContent); - } catch (IOException e) { - log.warn("Can not get the product.json from repo {} by path in {} version {}", repoName, - productJsonFilePath, versionTag); - } - return result; - } - - public String buildProductJsonFilePath(String version) { - String versionTag = "v" + version; - String pathToProductJsonFileFromTagContent = metaProductArtifact.getArtifactId(); - switch (productId) { - case NonStandardProductPackageConstants.PORTAL: - pathToProductJsonFileFromTagContent = "AxonIvyPortal/portal-product"; - versionTag = version; - break; - case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: - pathToProductJsonFileFromTagContent = "connectivity/connectivity-demos-product"; - break; - case NonStandardProductPackageConstants.ERROR_HANDLING: - pathToProductJsonFileFromTagContent = "error-handling/error-handling-demos-product"; - break; - case NonStandardProductPackageConstants.WORKFLOW_DEMO: - pathToProductJsonFileFromTagContent = "workflow/workflow-demos-product"; - break; - case NonStandardProductPackageConstants.MICROSOFT_365: - pathToProductJsonFileFromTagContent = "msgraph-connector-product/products/msgraph-connector"; - break; - case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: - pathToProductJsonFileFromTagContent = "html-dialog/html-dialog-demos-product"; - break; - case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: - pathToProductJsonFileFromTagContent = "rule-engine/rule-engine-demos-product"; - break; - default: - break; - } - productJsonFilePath = String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, - pathToProductJsonFileFromTagContent); - return versionTag; - } - - public MavenArtifactModel convertMavenArtifactToModel(MavenArtifact artifact, String version) { - String artifactName = artifact.getName(); - if (StringUtils.isBlank(artifactName)) { - artifactName = convertArtifactIdToName(artifact.getArtifactId()); - } - artifact.setType(Optional.ofNullable(artifact.getType()).orElse("iar")); - artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); - return new MavenArtifactModel(artifactName, buildDownloadUrlFromArtifactAndVersion(artifact, version), - artifact.getIsProductArtifact()); - } - - public List convertMavenArtifactsToModels(List artifacts, String version) { - List results = new ArrayList<>(); - if (!CollectionUtils.isEmpty(artifacts)) { - for (MavenArtifact artifact : artifacts) { - MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); - results.add(mavenArtifactModel); - } - } - return results; - } - - public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, String version) { - String groupIdByVersion = artifact.getGroupId(); - String artifactIdByVersion = artifact.getArtifactId(); - String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion( - artifact.getArtifactId(), version); - - if (Objects.nonNull(archivedArtifactBestMatchVersion)) { - groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); - artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); - } - groupIdByVersion = groupIdByVersion.replace(MavenConstants.DOT_SEPARATOR, - MavenConstants.GROUP_ID_URL_SEPARATOR); - return String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, repoUrl, groupIdByVersion, - artifactIdByVersion, version, artifactIdByVersion, version, artifact.getType()); - } - - public ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String artifactId, String version) { - List archivedArtifacts = archivedArtifactsMap.get(artifactId); - - if (CollectionUtils.isEmpty(archivedArtifacts)) { - return null; - } - for (ArchivedArtifact archivedArtifact : archivedArtifacts) { - if (latestVersionComparator.compare(archivedArtifact.getLastVersion(), version) <= 0) { - return archivedArtifact; - } - } - return null; - } - - public String convertArtifactIdToName(String artifactId) { - if (StringUtils.isBlank(artifactId)) { - return StringUtils.EMPTY; - } - return Arrays.stream(artifactId.split(MavenConstants.ARTIFACT_ID_SEPARATOR)) - .map(part -> part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase()) - .collect(Collectors.joining(MavenConstants.ARTIFACT_NAME_SEPARATOR)); - } - - public String getRepoNameFromMarketRepo(String fullRepoName) { - String[] repoNamePart = fullRepoName.split("/"); - return repoNamePart[repoNamePart.length - 1]; - } -} \ No newline at end of file + private final GHAxonIvyProductRepoService gitHubService; + private final MavenArtifactVersionRepository mavenArtifactVersionRepository; + private final ProductRepository productRepository; + @Getter + private String repoName; + private Map> archivedArtifactsMap; + private List artifactsFromMeta; + private MavenArtifactVersion proceedDataCache; + private MavenArtifact metaProductArtifact; + private final LatestVersionComparator latestVersionComparator = new LatestVersionComparator(); + @Getter + private String productJsonFilePath; + private String productId; + + public VersionServiceImpl(GHAxonIvyProductRepoService gitHubService, + MavenArtifactVersionRepository mavenArtifactVersionRepository, ProductRepository productRepository) { + this.gitHubService = gitHubService; + this.mavenArtifactVersionRepository = mavenArtifactVersionRepository; + this.productRepository = productRepository; + + } + + private void resetData() { + repoName = null; + archivedArtifactsMap = new HashMap<>(); + artifactsFromMeta = Collections.emptyList(); + proceedDataCache = null; + metaProductArtifact = null; + productJsonFilePath = null; + productId = null; + } + + public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, + String designerVersion) { + List results = new ArrayList<>(); + resetData(); + + this.productId = productId; + artifactsFromMeta = getProductMetaArtifacts(productId); + List versionsToDisplay = getVersionsToDisplay(isShowDevVersion, designerVersion); + proceedDataCache = mavenArtifactVersionRepository.findById(productId).orElse(new MavenArtifactVersion(productId)); + metaProductArtifact = artifactsFromMeta.stream() + .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findAny() + .orElse(new MavenArtifact()); + + sanitizeMetaArtifactBeforeHandle(); + + boolean isNewVersionDetected = handleArtifactForVersionToDisplay(versionsToDisplay, results); + if (isNewVersionDetected) { + mavenArtifactVersionRepository.save(proceedDataCache); + } + return results; + } + + public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, + List result) { + boolean isNewVersionDetected = false; + for (String version : versionsToDisplay) { + List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); + List productArtifactModels = + proceedDataCache.getProductArtifactWithVersionReleased().get(version); + if (productArtifactModels == null) { + isNewVersionDetected = true; + productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); + } + artifactsInVersion.addAll(productArtifactModels); + result.add(new MavenArtifactVersionModel(version, artifactsInVersion.stream().distinct().toList())); + } + return isNewVersionDetected; + } + + public List updateArtifactsInVersionWithProductArtifact(String version) { + List productArtifactModels = + convertMavenArtifactsToModels(getProductJsonByVersion(version), version); + proceedDataCache.getVersions().add(version); + proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); + return productArtifactModels; + } + + public List getProductMetaArtifacts(String productId) { + Product productInfo = productRepository.findById(productId).orElse(new Product()); + String fullRepoName = productInfo.getRepositoryName(); + if (StringUtils.isNotEmpty(fullRepoName)) { + repoName = getRepoNameFromMarketRepo(fullRepoName); + } + return Optional.ofNullable(productInfo.getArtifacts()).orElse(new ArrayList<>()); + } + + public void sanitizeMetaArtifactBeforeHandle() { + artifactsFromMeta.remove(metaProductArtifact); + artifactsFromMeta.forEach(artifact -> { + List archivedArtifacts = new ArrayList<>(Optional.ofNullable(artifact.getArchivedArtifacts()) + .orElse(Collections.emptyList()).stream().sorted(new ArchivedArtifactsComparator()).toList()); + Collections.reverse(archivedArtifacts); + archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); + }); + } + + @Override + public List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion) { + List versions = getVersionsFromMavenArtifacts(); + Stream versionStream = versions.stream(); + if (BooleanUtils.isTrue(isShowDevVersion)) { + return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) + .sorted(new LatestVersionComparator()).toList(); + } + if (StringUtils.isNotBlank(designerVersion)) { + return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); + } + return versions.stream().filter(this::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); + } + + public List getVersionsFromMavenArtifacts() { + Set versions = new HashSet<>(); + for (MavenArtifact artifact : artifactsFromMeta) { + versions.addAll( + getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), artifact.getArtifactId())); + Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()) + .forEach(archivedArtifact -> versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), + archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()))); + } + List versionList = new ArrayList<>(versions); + versionList.sort(new LatestVersionComparator()); + return versionList; + } + + @Override + public List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactID) { + List versions = new ArrayList<>(); + String baseUrl = buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactID); + if (StringUtils.isNotBlank(baseUrl)) { + versions.addAll(XmlReaderUtils.readXMLFromUrl(baseUrl)); + } + return versions; + } + + @Override + public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactID) { + if (StringUtils.isAnyBlank(groupId, artifactID)) { + return StringUtils.EMPTY; + } + repoUrl = Optional.ofNullable(repoUrl).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); + } + + public String getBugfixVersion(String version) { + + if (isSnapshotVersion(version)) { + version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); + } else if (isSprintVersion(version)) { + version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; + } + String[] segments = version.split("\\."); + if (segments.length >= 3) { + segments[2] = segments[2].split(CommonConstants.DASH_SEPARATOR)[0]; + return segments[0] + CommonConstants.DOT_SEPARATOR + segments[1] + CommonConstants.DOT_SEPARATOR + segments[2]; + } + return version; + } + + public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { + if (isReleasedVersion(version)) { + return true; + } + String bugfixVersion; + if (isSnapshotVersion(version)) { + bugfixVersion = getBugfixVersion(version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); + } else { + bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); + } + return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) + && isReleasedVersion(currentVersion) && getBugfixVersion(currentVersion).equals(bugfixVersion)); + } + + public boolean isSnapshotVersion(String version) { + return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); + } + + public boolean isSprintVersion(String version) { + return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); + } + + public boolean isReleasedVersion(String version) { + return !(isSprintVersion(version) || isSnapshotVersion(version)); + } + + public boolean isMatchWithDesignerVersion(String version, String designerVersion) { + return isReleasedVersion(version) && version.startsWith(designerVersion); + } + + public List getProductJsonByVersion(String version) { + List result = new ArrayList<>(); + String versionTag = getVersionTag(version); + productJsonFilePath = buildProductJsonFilePath(); + try { + GHContent productJsonContent = + gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, versionTag); + if (Objects.isNull(productJsonContent)) { + return result; + } + result = gitHubService.convertProductJsonToMavenProductInfo(productJsonContent); + } catch (IOException e) { + log.warn("Can not get the product.json from repo {} by path in {} version {}", repoName, productJsonFilePath, + versionTag); + } + return result; + } + + public String getVersionTag(String version) { + String versionTag = "v" + version; + if (NonStandardProductPackageConstants.PORTAL.equals(productId)) { + versionTag = version; + } + return versionTag; + } + + public String buildProductJsonFilePath() { + String pathToProductFolderFromTagContent = metaProductArtifact.getArtifactId(); + GitHubUtils.getNonStandardProductFilePath(productId); + productJsonFilePath = + String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, pathToProductFolderFromTagContent); + return productJsonFilePath; + } + + public MavenArtifactModel convertMavenArtifactToModel(MavenArtifact artifact, String version) { + String artifactName = artifact.getName(); + if (StringUtils.isBlank(artifactName)) { + artifactName = GitHubUtils.convertArtifactIdToName(artifact.getArtifactId()); + } + artifact.setType(Optional.ofNullable(artifact.getType()).orElse("iar")); + artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); + return new MavenArtifactModel(artifactName, buildDownloadUrlFromArtifactAndVersion(artifact, version), + artifact.getIsProductArtifact()); + } + + public List convertMavenArtifactsToModels(List artifacts, String version) { + List results = new ArrayList<>(); + if (!CollectionUtils.isEmpty(artifacts)) { + for (MavenArtifact artifact : artifacts) { + MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); + results.add(mavenArtifactModel); + } + } + return results; + } + + public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, String version) { + String groupIdByVersion = artifact.getGroupId(); + String artifactIdByVersion = artifact.getArtifactId(); + String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + ArchivedArtifact archivedArtifactBestMatchVersion = + findArchivedArtifactInfoBestMatchWithVersion(artifact.getArtifactId(), version); + + if (Objects.nonNull(archivedArtifactBestMatchVersion)) { + groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); + artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); + } + groupIdByVersion = groupIdByVersion.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, repoUrl, groupIdByVersion, artifactIdByVersion, + version, artifactIdByVersion, version, artifact.getType()); + } + + public ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String artifactId, String version) { + List archivedArtifacts = archivedArtifactsMap.get(artifactId); + + if (CollectionUtils.isEmpty(archivedArtifacts)) { + return null; + } + for (ArchivedArtifact archivedArtifact : archivedArtifacts) { + if (latestVersionComparator.compare(archivedArtifact.getLastVersion(), version) <= 0) { + return archivedArtifact; + } + } + return null; + } + + public String getRepoNameFromMarketRepo(String fullRepoName) { + String[] repoNamePart = fullRepoName.split("/"); + return repoNamePart[repoNamePart.length - 1]; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java new file mode 100644 index 000000000..d49802145 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java @@ -0,0 +1,58 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.MavenConstants; +import lombok.extern.log4j.Log4j2; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Log4j2 +public class XmlReaderUtils { + private static final RestTemplate restTemplate = new RestTemplate(); + + private XmlReaderUtils() {} + + public static List readXMLFromUrl(String url) { + List versions = new ArrayList<>(); + try { + String xmlData = restTemplate.getForObject(url, String.class); + extractVersions(xmlData, versions); + } catch (HttpClientErrorException e) { + log.error(e.getMessage()); + } + return versions; + } + + public static void extractVersions(String xmlData, List versions) { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(xmlData))); + + XPath xpath = XPathFactory.newInstance().newXPath(); + XPathExpression expr = xpath.compile(MavenConstants.VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE); + + Object result = expr.evaluate(document, XPathConstants.NODESET); + NodeList versionNodes = (NodeList) result; + + for (int i = 0; i < versionNodes.getLength(); i++) { + versions.add(Optional.ofNullable(versionNodes.item(i)).map(Node::getTextContent).orElse(null)); + } + } catch (Exception e) { + log.error(e.getMessage()); + } + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java deleted file mode 100644 index 8f7f6a6bc..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.axonivy.market.utils; - -import com.axonivy.market.constants.MavenConstants; -import lombok.extern.log4j.Log4j2; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathFactory; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Log4j2 -public class XmlReaderUtils { - private static final RestTemplate restTemplate = new RestTemplate(); - - private XmlReaderUtils() { - } - - public static List readXMLFromUrl(String url) { - List versions = new ArrayList<>(); - try { - String xmlData = restTemplate.getForObject(url, String.class); - extractVersions(xmlData, versions); - } catch (HttpClientErrorException e) { - log.error(e.getMessage()); - } - return versions; - } - - public static void extractVersions(String xmlData, List versions) { - try { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document document = builder.parse(new InputSource(new StringReader(xmlData))); - - XPath xpath = XPathFactory.newInstance().newXPath(); - XPathExpression expr = xpath.compile(MavenConstants.VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE); - - Object result = expr.evaluate(document, XPathConstants.NODESET); - NodeList versionNodes = (NodeList) result; - - for (int i = 0; i < versionNodes.getLength(); i++) { - versions.add(Optional.ofNullable(versionNodes.item(i)).map(Node::getTextContent).orElse(null)); - } - } catch (Exception e) { - log.error(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 1779befd8..814044478 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -1,13 +1,17 @@ package com.axonivy.market.controller; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.service.VersionService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; + import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -17,28 +21,81 @@ import java.util.List; import java.util.Objects; +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; + @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { - - @InjectMocks - private ProductDetailsController productDetailsController; + @Mock + private ProductService productService; @Mock VersionService versionService; + @Mock + private ProductDetailModelAssembler detailModelAssembler; + + @InjectMocks + private ProductDetailsController productDetailsController; + private static final String PRODUCT_NAME_SAMPLE = "Docker"; + private static final String PRODUCT_NAME_DE_SAMPLE = "Docker DE"; + @Test - void testFindProduct() { - var result = productDetailsController.findProduct("", ""); - assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + void testProductDetails() { + Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), null)).thenReturn(createProductMockWithDetails()); + ResponseEntity mockExpectedResult = + new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); + + ResponseEntity result = productDetailsController.findProductDetails("docker-connector"); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(result, mockExpectedResult); + + verify(productService, times(1)).fetchProductDetail("docker-connector"); + verify(detailModelAssembler, times(1)).toModel(mockProduct(), null); } @Test - void testFindProductVersionsById(){ + void testFindProductVersionsById() { List models = List.of(new MavenArtifactVersionModel()); - Mockito.when(versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())).thenReturn(models); - ResponseEntity> result = productDetailsController.findProductVersionsById("protal", true, "10.0.1"); + Mockito.when( + versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())) + .thenReturn(models); + ResponseEntity> result = + productDetailsController.findProductVersionsById("protal", true, "10.0.1"); Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()); Assertions.assertEquals(1, Objects.requireNonNull(result.getBody()).size()); Assertions.assertEquals(models, result.getBody()); } -} \ No newline at end of file + + private Product mockProduct() { + Product mockProduct = new Product(); + mockProduct.setId("docker-connector"); + MultilingualismValue name = new MultilingualismValue(); + name.setEn(PRODUCT_NAME_SAMPLE); + name.setDe(PRODUCT_NAME_DE_SAMPLE); + mockProduct.setNames(name); + mockProduct.setLanguage("English"); + return mockProduct; + } + + private ProductDetailModel createProductMockWithDetails() { + ProductDetailModel mockProductDetail = new ProductDetailModel(); + mockProductDetail.setId("docker-connector"); + MultilingualismValue name = new MultilingualismValue(); + name.setEn(PRODUCT_NAME_SAMPLE); + name.setDe(PRODUCT_NAME_DE_SAMPLE); + mockProductDetail.setNames(name); + mockProductDetail.setType("connector"); + mockProductDetail.setCompatibility("10.0+"); + mockProductDetail.setSourceUrl("https://github.com/axonivy-market/docker-connector"); + mockProductDetail.setStatusBadgeUrl("https://github.com/axonivy-market/docker-connector"); + mockProductDetail.setLanguage("English"); + mockProductDetail.setIndustry("Cross-Industry"); + mockProductDetail.setContactUs(false); + return mockProductDetail; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index 0221c4ce6..dd770b752 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -1,7 +1,7 @@ package com.axonivy.market.factory; -import static com.axonivy.market.constants.CommonConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.Mockito.mock; @@ -31,7 +31,7 @@ void testMappingByGHContent() throws IOException { GHContent mockContent = mock(GHContent.class); var result = ProductFactory.mappingByGHContent(product, null); assertEquals(product, result); - when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); + when(mockContent.getName()).thenReturn(META_FILE); InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); when(mockContent.read()).thenReturn(inputStream); result = ProductFactory.mappingByGHContent(product, mockContent); diff --git a/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java deleted file mode 100644 index 28293329d..000000000 --- a/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java +++ /dev/null @@ -1,242 +0,0 @@ -package com.axonivy.market.github.service; - -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.*; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class GHAxonIvyProductRepoServiceImplTest { - - private static final String DUMMY_TAG = "v1.0.0"; - - @Mock - PagedIterable listTags; - - @Mock - GHRepository ghRepository; - - @Mock - GitHubService githubService; - - @Mock - GHOrganization organization; - - @Mock - JsonNode dataNode; - - @Mock - JsonNode childNode; - - @Mock - GHContent content = new GHContent(); - - @InjectMocks - @Spy - private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; - - void setup() throws IOException { - var mockGHOrganization = mock(GHOrganization.class); - when(githubService.getOrganization(any())).thenReturn(mockGHOrganization); - when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); - } - - @Test - void testAllTagsFromRepoName() throws IOException { - setup(); - var mockTag = mock(GHTag.class); - when(mockTag.getName()).thenReturn(DUMMY_TAG); - when(listTags.toList()).thenReturn(List.of(mockTag)); - when(ghRepository.listTags()).thenReturn(listTags); - var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); - assertEquals(1, result.size()); - assertEquals(DUMMY_TAG, result.get(0).getName()); - } - - @Test - void testContentFromGHRepoAndTag() throws IOException { - setup(); - var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); - assertNull(result); - when(axonivyProductRepoServiceImpl.getOrganization()).thenThrow(IOException.class); - result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); - assertNull(result); - } - - @Test - void testExtractMavenArtifactFromJsonNode() { - List artifacts = new ArrayList<>(); - boolean isDependency = true; - String nodeName = ProductJsonConstants.DEPENDENCIES; - - createListNodeForDataNoteByName(nodeName); - MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, - isDependency); - - axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); - - assertEquals(1, artifacts.size()); - assertSame(mockArtifact, artifacts.get(0)); - - isDependency = false; - nodeName = ProductJsonConstants.PROJECTS; - createListNodeForDataNoteByName(nodeName); - - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, - isDependency); - - axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); - - assertEquals(2, artifacts.size()); - assertSame(mockArtifact, artifacts.get(1)); - } - - private void createListNodeForDataNoteByName(String nodeName) { - JsonNode sectionNode = Mockito.mock(JsonNode.class); - Iterator iterator = Mockito.mock(Iterator.class); - Mockito.when(dataNode.path(nodeName)).thenReturn(sectionNode); - Mockito.when(sectionNode.iterator()).thenReturn(iterator); - Mockito.when(iterator.hasNext()).thenReturn(true, false); - Mockito.when(iterator.next()).thenReturn(childNode); - } - - @Test - void testCreateArtifactFromJsonNode() { - String repoUrl = "http://example.com/repo"; - boolean isDependency = true; - String groupId = "com.example"; - String artifactId = "example-artifact"; - String type = "jar"; - - JsonNode groupIdNode = Mockito.mock(JsonNode.class); - JsonNode artifactIdNode = Mockito.mock(JsonNode.class); - JsonNode typeNode = Mockito.mock(JsonNode.class); - Mockito.when(groupIdNode.asText()).thenReturn(groupId); - Mockito.when(artifactIdNode.asText()).thenReturn(artifactId); - Mockito.when(typeNode.asText()).thenReturn(type); - Mockito.when(dataNode.path(ProductJsonConstants.GROUP_ID)).thenReturn(groupIdNode); - Mockito.when(dataNode.path(ProductJsonConstants.ARTIFACT_ID)).thenReturn(artifactIdNode); - Mockito.when(dataNode.path(ProductJsonConstants.TYPE)).thenReturn(typeNode); - - MavenArtifact artifact = axonivyProductRepoServiceImpl.createArtifactFromJsonNode(dataNode, repoUrl, - isDependency); - - assertEquals(repoUrl, artifact.getRepoUrl()); - assertTrue(artifact.getIsDependency()); - assertEquals(groupId, artifact.getGroupId()); - assertEquals(artifactId, artifact.getArtifactId()); - assertEquals(type, artifact.getType()); - assertTrue(artifact.getIsProductArtifact()); - } - - @Test - void testConvertProductJsonToMavenProductInfo() throws IOException { - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(null).size()); - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - - InputStream inputStream = getMockInputStream(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); - assertEquals(2, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - inputStream = getMockInputStreamWithOutProjectAndDependency(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - } - - private static InputStream getMockInputStream() { - String jsonContent = """ - { - "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", - "installers": [ - { - "id": "maven-import", - "data": { - "projects": [ - { - "groupId": "com.axonivy.utils.bpmnstatistic", - "artifactId": "bpmn-statistic-demo", - "version": "${version}", - "type": "iar" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } - }, - { - "id": "maven-dependency", - "data": { - "dependencies": [ - { - "groupId": "com.axonivy.utils.bpmnstatistic", - "artifactId": "bpmn-statistic", - "version": "${version}", - "type": "iar" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } - } - ] - } - """; - return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); - } - - private static InputStream getMockInputStreamWithOutProjectAndDependency() { - String jsonContent = "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" - + " \"repositories\": [\n" + " {\n" - + " \"url\": \"http://example.com/repo\"\n" + " }\n" + " ]\n" + " }\n" - + " }\n" + " ]\n" + "}"; - return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); - } - - @Test - void testExtractedContentStream() { - assertNull(axonivyProductRepoServiceImpl.extractedContentStream(null)); - assertNull(axonivyProductRepoServiceImpl.extractedContentStream(content)); - } - - @Test - void testGetOrganization() throws IOException { - Mockito.when(githubService.getOrganization(Mockito.anyString())).thenReturn(organization); - assertEquals(organization, axonivyProductRepoServiceImpl.getOrganization()); - assertEquals(organization, axonivyProductRepoServiceImpl.getOrganization()); - } -} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java new file mode 100644 index 000000000..fa0104d4a --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java @@ -0,0 +1,375 @@ +package com.axonivy.market.service; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GHAxonIvyProductRepoServiceImplTest { + + private static final String DUMMY_TAG = "v1.0.0"; + public static final String RELEASE_TAG = "v10.0.0"; + public static final String IMAGE_NAME = "image.png"; + public static final String DOCUWARE_CONNECTOR_PRODUCT = "docuware-connector-product"; + public static final String IMAGE_DOWNLOAD_URL = "https://raw.githubusercontent.com/image.png"; + + @Mock + PagedIterable listTags; + + @Mock + GHRepository ghRepository; + + @Mock + GitHubService gitHubService; + + GHOrganization mockGHOrganization = mock(GHOrganization.class); + + @Mock + JsonNode dataNode; + + @Mock + JsonNode childNode; + + @Mock + GHContent content = new GHContent(); + + @InjectMocks + @Spy + private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; + + void setup() throws IOException { + when(gitHubService.getOrganization(any())).thenReturn(mockGHOrganization); + when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); + } + + @AfterEach + void after() throws IOException { + reset(mockGHOrganization); + reset(gitHubService); + } + + @Test + void testAllTagsFromRepoName() throws IOException { + setup(); + var mockTag = mock(GHTag.class); + when(mockTag.getName()).thenReturn(DUMMY_TAG); + when(listTags.toList()).thenReturn(List.of(mockTag)); + when(ghRepository.listTags()).thenReturn(listTags); + var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); + assertEquals(1, result.size()); + assertEquals(DUMMY_TAG, result.get(0).getName()); + } + + @Test + void testContentFromGHRepoAndTag() throws IOException { + setup(); + var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertNull(result); + when(axonivyProductRepoServiceImpl.getOrganization()).thenThrow(IOException.class); + result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertNull(result); + } + + @Test + void testExtractMavenArtifactFromJsonNode() { + List artifacts = new ArrayList<>(); + boolean isDependency = true; + String nodeName = ProductJsonConstants.DEPENDENCIES; + + createListNodeForDataNoteByName(nodeName); + MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, + isDependency); + + axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + + assertEquals(1, artifacts.size()); + assertSame(mockArtifact, artifacts.get(0)); + + isDependency = false; + nodeName = ProductJsonConstants.PROJECTS; + createListNodeForDataNoteByName(nodeName); + + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, + isDependency); + + axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + + assertEquals(2, artifacts.size()); + assertSame(mockArtifact, artifacts.get(1)); + } + + private void createListNodeForDataNoteByName(String nodeName) { + JsonNode sectionNode = Mockito.mock(JsonNode.class); + Iterator iterator = Mockito.mock(Iterator.class); + Mockito.when(dataNode.path(nodeName)).thenReturn(sectionNode); + Mockito.when(sectionNode.iterator()).thenReturn(iterator); + Mockito.when(iterator.hasNext()).thenReturn(true, false); + Mockito.when(iterator.next()).thenReturn(childNode); + } + + @Test + void testCreateArtifactFromJsonNode() { + String repoUrl = "http://example.com/repo"; + boolean isDependency = true; + String groupId = "com.example"; + String artifactId = "example-artifact"; + String type = "jar"; + + JsonNode groupIdNode = Mockito.mock(JsonNode.class); + JsonNode artifactIdNode = Mockito.mock(JsonNode.class); + JsonNode typeNode = Mockito.mock(JsonNode.class); + Mockito.when(groupIdNode.asText()).thenReturn(groupId); + Mockito.when(artifactIdNode.asText()).thenReturn(artifactId); + Mockito.when(typeNode.asText()).thenReturn(type); + Mockito.when(dataNode.path(ProductJsonConstants.GROUP_ID)).thenReturn(groupIdNode); + Mockito.when(dataNode.path(ProductJsonConstants.ARTIFACT_ID)).thenReturn(artifactIdNode); + Mockito.when(dataNode.path(ProductJsonConstants.TYPE)).thenReturn(typeNode); + + MavenArtifact artifact = axonivyProductRepoServiceImpl.createArtifactFromJsonNode(dataNode, repoUrl, isDependency); + + assertEquals(repoUrl, artifact.getRepoUrl()); + assertTrue(artifact.getIsDependency()); + assertEquals(groupId, artifact.getGroupId()); + assertEquals(artifactId, artifact.getArtifactId()); + assertEquals(type, artifact.getType()); + assertTrue(artifact.getIsProductArtifact()); + } + + @Test + void testConvertProductJsonToMavenProductInfo() throws IOException { + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(null).size()); + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + + InputStream inputStream = getMockInputStream(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(2, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + inputStream = getMockInputStreamWithOutProjectAndDependency(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + } + + @Test + void testExtractedContentStream() { + assertNull(axonivyProductRepoServiceImpl.extractedContentStream(null)); + assertNull(axonivyProductRepoServiceImpl.extractedContentStream(content)); + } + + @Test + void testGetOrganization() throws IOException { + Mockito.when(gitHubService.getOrganization(Mockito.anyString())).thenReturn(mockGHOrganization); + assertEquals(mockGHOrganization, axonivyProductRepoServiceImpl.getOrganization()); + assertEquals(mockGHOrganization, axonivyProductRepoServiceImpl.getOrganization()); + } + + @Test + void testGetReadmeAndProductContentsFromTag() throws IOException { + String readmeContentWithImage = + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + + GHContent mockContent = createMockProductFolderWithProductJson(); + + getReadmeInputStream(readmeContentWithImage, mockContent); + InputStream inputStream = getMockInputStream(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(any())).thenReturn(inputStream); + var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, + RELEASE_TAG); + + assertEquals(RELEASE_TAG, result.getTag()); + assertTrue(result.getIsDependency()); + assertEquals("com.axonivy.utils.bpmnstatistic", result.getGroupId()); + assertEquals("bpmn-statistic", result.getArtifactId()); + assertEquals("iar", result.getType()); + assertEquals("Test README", result.getDescription()); + assertEquals("Demo content", result.getDemo()); + assertEquals("Setup content (https://raw.githubusercontent.com/image.png)", result.getSetup()); + } + + @Test + void testGetReadmeAndProductContentFromTag_ImageFromFolder() throws IOException { + String readmeContentWithImageFolder = + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (./images/image.png)"; + + GHContent mockImageFile = mock(GHContent.class); + when(mockImageFile.getName()).thenReturn(ReadmeConstants.IMAGES, IMAGE_NAME); + when(mockImageFile.isDirectory()).thenReturn(true); + when(mockImageFile.getDownloadUrl()).thenReturn(IMAGE_DOWNLOAD_URL); + + PagedIterable pagedIterable = mock(PagedIterable.class); + when(mockImageFile.listDirectoryContent()).thenReturn(pagedIterable); + when(pagedIterable.toList()).thenReturn(List.of(mockImageFile)); + + String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(createMockProduct(), + List.of(mockImageFile), readmeContentWithImageFolder); + + assertEquals( + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (https://raw.githubusercontent.com/image.png)", + updatedReadme); + } + + @Test + void testGetReadmeAndProductContentsFromTag_WithNoFullyThreeParts() throws IOException { + String readmeContentString = "#Product-name\n Test README\n## Setup\nSetup content"; + + GHContent mockContent = createMockProductFolder(); + + getReadmeInputStream(readmeContentString, mockContent); + + var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, + RELEASE_TAG); + + assertNull(result.getArtifactId()); + assertEquals("Setup content", result.getSetup()); + } + + @Test + void testGetReadmeAndProductContentsFromTag_SwitchPartsPosition() throws IOException { + String readmeContentString = "#Product-name\n Test README\n## Setup\nSetup content\n## Demo\nDemo content"; + + GHContent mockContent = createMockProductFolder(); + + getReadmeInputStream(readmeContentString, mockContent); + + var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, + RELEASE_TAG); + assertEquals("Demo content", result.getDemo()); + assertEquals("Setup content", result.getSetup()); + } + + private static void getReadmeInputStream(String readmeContentString, GHContent mockContent) throws IOException { + InputStream mockReadmeInputStream = mock(InputStream.class); + when(mockContent.read()).thenReturn(mockReadmeInputStream); + when(mockReadmeInputStream.readAllBytes()).thenReturn(readmeContentString.getBytes()); + } + + private static InputStream getMockInputStream() { + String jsonContent = """ + { + "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", + "installers": [ + { + "id": "maven-import", + "data": { + "projects": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic-demo", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + } + ] + } + """; + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + private static InputStream getMockInputStreamWithOutProjectAndDependency() { + String jsonContent = "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" + + " \"repositories\": [\n" + " {\n" + " \"url\": \"http://example.com/repo\"\n" + + " }\n" + " ]\n" + " }\n" + " }\n" + " ]\n" + "}"; + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + private Product createMockProduct() throws IOException { + Product product = new Product(); + product.setId("docuware-connector"); + product.setLanguage("en"); + return product; + } + + private GHContent createMockProductFolder() throws IOException { + GHContent mockContent = mock(GHContent.class); + when(mockContent.isDirectory()).thenReturn(true); + when(mockContent.isFile()).thenReturn(true); + when(mockContent.getName()).thenReturn(DOCUWARE_CONNECTOR_PRODUCT, ReadmeConstants.README_FILE); + + when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)).thenReturn(List.of(mockContent)); + when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)).thenReturn(List.of(mockContent)); + + return mockContent; + } + + private GHContent createMockProductFolderWithProductJson() throws IOException { + GHContent mockContent = mock(GHContent.class); + when(mockContent.isDirectory()).thenReturn(true); + when(mockContent.isFile()).thenReturn(true); + when(mockContent.getName()).thenReturn(DOCUWARE_CONNECTOR_PRODUCT, ReadmeConstants.README_FILE); + + GHContent mockContent2 = createMockProductJson(); + + when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)) + .thenReturn(List.of(mockContent, mockContent2)); + when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)) + .thenReturn(List.of(mockContent, mockContent2)); + + return mockContent; + } + + private static GHContent createMockProductJson() throws IOException { + GHContent mockProductJson = mock(GHContent.class); + when(mockProductJson.isFile()).thenReturn(true); + when(mockProductJson.getName()).thenReturn(ProductJsonConstants.PRODUCT_JSON_FILE, IMAGE_NAME); + when(mockProductJson.getDownloadUrl()).thenReturn(IMAGE_DOWNLOAD_URL); + return mockProductJson; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index bcebfd4e8..9fd472991 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -1,32 +1,30 @@ package com.axonivy.market.service; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.META_FILE; +import static com.axonivy.market.constants.MetaConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.model.MultilingualismValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHContent; +import org.kohsuke.github.*; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -46,7 +44,6 @@ import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.ProductServiceImpl; @@ -57,13 +54,17 @@ class ProductServiceImplTest { private static final String SAMPLE_PRODUCT_ID = "amazon-comprehend"; private static final String SAMPLE_PRODUCT_NAME = "Amazon Comprehend"; private static final long LAST_CHANGE_TIME = 1718096290000l; - private static final Pageable PAGEABLE = PageRequest.of(0, 20, - Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); + private static final Pageable PAGEABLE = + PageRequest.of(0, 20, Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; + public static final String RELEASE_TAG = "v10.0.2"; private String keyword; private String langague; private Page mockResultReturn; + @Mock + private GHRepository ghRepository; + @Mock private ProductRepository productRepository; @@ -76,6 +77,12 @@ class ProductServiceImplTest { @Mock private GitHubService gitHubService; + @Mock + private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; + + @Captor + ArgumentCaptor> argumentCaptor; + @InjectMocks private ProductServiceImpl productService; @@ -188,9 +195,14 @@ void testFindAllProductsWithKeyword() throws IOException { assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); // Test has keyword and type is connector - when(productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))).thenReturn( - new PageImpl<>(mockResultReturn.stream().filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) - && product.getType().equals(TypeOption.CONNECTORS.getCode())).collect(Collectors.toList()))); + when( + productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))) + .thenReturn( + new PageImpl<>( + mockResultReturn.stream() + .filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) + && product.getType().equals(TypeOption.CONNECTORS.getCode())) + .collect(Collectors.toList()))); // Executes result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); assertTrue(result.hasContent()); @@ -202,6 +214,20 @@ void testSyncProductsFirstTime() throws IOException { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())) + .thenReturn(mockReadmeProductContent()); + when(gitHubService.getRepository(any())).thenReturn(ghRepository); + PagedIterable pagedIterable = mock(PagedIterable.class); + when(ghRepository.listTags()).thenReturn(pagedIterable); + + GHTag mockTag = mock(GHTag.class); + GHCommit mockGHCommit = mock(GHCommit.class); + + when(mockTag.getName()).thenReturn(RELEASE_TAG); + when(mockTag.getCommit()).thenReturn(mockGHCommit); + when(mockGHCommit.getCommitDate()).thenReturn(new Date()); + + when(pagedIterable.toList()).thenReturn(List.of(mockTag)); var mockContent = mockGHContentAsMetaJSON(); InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); @@ -212,8 +238,12 @@ void testSyncProductsFirstTime() throws IOException { when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); // Executes - var result = productService.syncLatestDataFromMarketRepo(); - assertEquals(false, result); + productService.syncLatestDataFromMarketRepo(); + + verify(productRepository).saveAll(argumentCaptor.capture()); + + assertThat(argumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() + .isEqualTo(List.of(mockReadmeProductContent())); } @Test @@ -235,13 +265,37 @@ void testSearchProducts() { String type = TypeOption.ALL.getOption(); keyword = "on"; langague = "en"; - when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)).thenReturn(mockResultReturn); + when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)) + .thenReturn(mockResultReturn); var result = productService.findProducts(type, keyword, langague, simplePageable); assertEquals(result, mockResultReturn); verify(productRepository).searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable); } + @Test + void testFetchProductDetail() { + String id = "amazon-comprehend"; + Product mockProduct = mockResultReturn.getContent().get(0); + when(productRepository.findById(id)).thenReturn(Optional.ofNullable(mockProduct)); + Product result = productService.fetchProductDetail(id); + assertEquals(mockProduct, result); + verify(productRepository, times(1)).findById(id); + } + + @Test + void testGetCompatibilityFromNumericTag() { + + String result = productService.getCompatibilityFromOldestTag("1.0.0"); + assertEquals("1.0+", result); + + result = productService.getCompatibilityFromOldestTag("8"); + assertEquals("8.0+", result); + + result = productService.getCompatibilityFromOldestTag("11.2"); + assertEquals("11.2+", result); + } + private Page createPageProductsMock() { var mockProducts = new ArrayList(); MultilingualismValue name = new MultilingualismValue(); @@ -282,4 +336,12 @@ private GHContent mockGHContentAsMetaJSON() { when(mockGHContent.getName()).thenReturn(META_FILE); return mockGHContent; } + + private ProductModuleContent mockReadmeProductContent() { + ProductModuleContent productModuleContent = new ProductModuleContent(); + productModuleContent.setTag("v10.0.2"); + productModuleContent.setName("Amazon Comprehend"); + productModuleContent.setDescription("testDescription"); + return productModuleContent; + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java index f9f75b439..ce3ef83ee 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java @@ -1,33 +1,6 @@ package com.axonivy.market.service; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.api.Fail; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHContent; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - import com.axonivy.market.constants.MavenConstants; -import com.axonivy.market.constants.NonStandardProductPackageConstants; import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.entity.MavenArtifactVersion; import com.axonivy.market.entity.Product; @@ -38,535 +11,486 @@ import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.VersionServiceImpl; -import com.axonivy.market.utils.XmlReaderUtils; +import com.axonivy.market.util.XmlReaderUtils; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Fail; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.*; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.util.*; @ExtendWith(MockitoExtension.class) class VersionServiceImplTest { - private String repoName; - private Map> archivedArtifactsMap; - private List artifactsFromMeta; - private MavenArtifactVersion proceedDataCache; - private MavenArtifact metaProductArtifact; - @Spy - @InjectMocks - private VersionServiceImpl versionService; - - @Mock - private GHAxonIvyProductRepoService gitHubService; - - @Mock - private MavenArtifactVersionRepository mavenArtifactVersionRepository; - - @Mock - private ProductRepository productRepository; - - @BeforeEach() - void prepareBeforeTest() { - archivedArtifactsMap = new HashMap<>(); - artifactsFromMeta = new ArrayList<>(); - metaProductArtifact = new MavenArtifact(); - proceedDataCache = new MavenArtifactVersion(); - repoName = StringUtils.EMPTY; - ReflectionTestUtils.setField(versionService, "archivedArtifactsMap", archivedArtifactsMap); - ReflectionTestUtils.setField(versionService, "artifactsFromMeta", artifactsFromMeta); - ReflectionTestUtils.setField(versionService, "proceedDataCache", proceedDataCache); - ReflectionTestUtils.setField(versionService, "metaProductArtifact", metaProductArtifact); - } - - private void setUpArtifactFromMeta() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - metaProductArtifact.setGroupId(groupId); - metaProductArtifact.setArtifactId(artifactId); - metaProductArtifact.setIsProductArtifact(true); - MavenArtifact additionalMavenArtifact = new MavenArtifact(repoUrl, "", groupId, artifactId, "", null, null, - null); - artifactsFromMeta.add(metaProductArtifact); - artifactsFromMeta.add(additionalMavenArtifact); - } - - @Test - void testGetArtifactsAndVersionToDisplay() { - String productId = "adobe-acrobat-sign-connector"; - String targetVersion = "10.0.10"; - setUpArtifactFromMeta(); - when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); - when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())) - .thenReturn(List.of(targetVersion)); - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); - ArrayList artifactsInVersion = new ArrayList<>(); - artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(artifactsInVersion); - Assertions.assertEquals(1, - versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); - - MavenArtifactVersion proceededData = new MavenArtifactVersion(); - proceededData.getProductArtifactWithVersionReleased().put(targetVersion, new ArrayList<>()); - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); - Assertions.assertEquals(1, - versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); - } - - @Test - void testHandleArtifactForVersionToDisplay() { - String newVersionDetected = "10.0.10"; - List result = new ArrayList<>(); - List versionsToDisplay = List.of(newVersionDetected); - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - Assertions.assertTrue(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(newVersionDetected, result.get(0).getVersion()); - Assertions.assertEquals(0, result.get(0).getArtifactsByVersion().size()); - - result = new ArrayList<>(); - ArrayList artifactsInVersion = new ArrayList<>(); - artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(artifactsInVersion); - Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); - } - - @Test - void testGetProductMetaArtifacts() { - Product product = new Product(); - MavenArtifact artifact1 = new MavenArtifact(); - MavenArtifact artifact2 = new MavenArtifact(); - List artifacts = List.of(artifact1, artifact2); - product.setArtifacts(artifacts); - when(productRepository.findById(Mockito.anyString())).thenReturn(Optional.of(product)); - List result = versionService.getProductMetaArtifacts("portal"); - Assertions.assertEquals(artifacts, result); - Assertions.assertNull(versionService.getRepoName()); - - product.setRepositoryName("/market/portal"); - versionService.getProductMetaArtifacts("portal"); - Assertions.assertEquals("portal", versionService.getRepoName()); - } - - @Test - void testUpdateArtifactsInVersionWithProductArtifact() { - String version = "10.0.10"; - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifactModel artifactModel = new MavenArtifactModel(); - List mockMavenArtifactModels = List.of(artifactModel); - when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(mockMavenArtifactModels); - Assertions.assertEquals(mockMavenArtifactModels, - versionService.updateArtifactsInVersionWithProductArtifact(version)); - Assertions.assertEquals(1, proceedDataCache.getVersions().size()); - Assertions.assertEquals(1, proceedDataCache.getProductArtifactWithVersionReleased().size()); - Assertions.assertEquals(version, proceedDataCache.getVersions().get(0)); - } - - @Test - void testSanitizeMetaArtifactBeforeHandle() { - setUpArtifactFromMeta(); - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String archivedArtifactId1 = "adobe-acrobat-sign-connector"; - String archivedArtifactId2 = "adobe-acrobat-sign-connector"; - ArchivedArtifact archivedArtifact1 = new ArchivedArtifact("10.0.10", groupId, archivedArtifactId1); - ArchivedArtifact archivedArtifact2 = new ArchivedArtifact("10.0.20", groupId, archivedArtifactId2); - artifactsFromMeta.get(1).setArchivedArtifacts(List.of(archivedArtifact2, archivedArtifact1)); - - versionService.sanitizeMetaArtifactBeforeHandle(); - String artifactId = "adobe-acrobat-sign-connector"; - - Assertions.assertEquals(1, artifactsFromMeta.size()); - Assertions.assertEquals(1, archivedArtifactsMap.size()); - Assertions.assertEquals(2, archivedArtifactsMap.get(artifactId).size()); - Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); - } - - @Test - void testGetVersionsToDisplay() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - versionFromArtifact.add("10.0.3-SNAPSHOT"); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)) - .thenReturn(versionFromArtifact); - Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(true, null)); - Assertions.assertEquals(List.of("10.0.5"), versionService.getVersionsToDisplay(null, "10.0.5")); - versionFromArtifact.remove("10.0.3-SNAPSHOT"); - Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(null, null)); - } - - @Test - void getVersionsFromMavenArtifacts() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - String archivedArtifactId = "adobe-sign-connector"; - artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)) - .thenReturn(versionFromArtifact); - Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); - - List archivedArtifacts = List.of(new ArchivedArtifact("10.0.9", groupId, archivedArtifactId)); - ArrayList versionFromArchivedArtifact = new ArrayList<>(); - versionFromArchivedArtifact.add("10.0.3"); - versionFromArchivedArtifact.add("10.0.2"); - versionFromArchivedArtifact.add("10.0.1"); - artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)) - .thenReturn(versionFromArchivedArtifact); - versionFromArtifact.addAll(versionFromArchivedArtifact); - Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); - } - - @Test - void testGetVersionsFromArtifactDetails() { - - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.16"); - versionFromArtifact.add("10.0.18"); - versionFromArtifact.add("10.0.19"); - versionFromArtifact.add("10.0.20"); - versionFromArtifact.add("10.0.21"); - - try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { - xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); - } - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), - versionFromArtifact); - } - - @Test - void testBuildMavenMetadataUrlFromArtifact() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - String metadataUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; - Assertions.assertEquals(StringUtils.EMPTY, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); - Assertions.assertEquals(StringUtils.EMPTY, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), StringUtils.EMPTY); - Assertions.assertEquals(metadataUrl, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); - } - - @Test - void testIsReleasedVersionOrUnReleaseDevVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - String unreleasedSprintVersion = "10.0.21-m1235"; - List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); - Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); - Assertions - .assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); - } - - @Test - void testGetBugfixVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(releasedVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(snapshotVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(sprintVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(minorSprintVersion)); - } - - @Test - void testIsSnapshotVersion() { - String targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertTrue(versionService.isSnapshotVersion(targetVersion)); - - targetVersion = "10.0.21-m1234"; - Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); - } - - @Test - void testIsSprintVersion() { - String targetVersion = "10.0.21-m1234"; - Assertions.assertTrue(versionService.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); - } - - @Test - void testIsReleasedVersion() { - String targetVersion = "10.0.21"; - Assertions.assertTrue(versionService.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-m1231"; - Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); - } - - @Test - void testIsMatchWithDesignerVersion() { - String designerVersion = "10.0.21"; - String targetVersion = "10.0.21.2"; - Assertions.assertTrue(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.19"; - Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - } - - @Test - void testGetProductJsonByVersion() { - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetGroupId = "com.axonivy.connector.adobe.acrobat"; - GHContent mockContent = mock(GHContent.class); - repoName = "adobe-acrobat-sign-connector"; - ReflectionTestUtils.setField(versionService, "repoName", repoName); - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifact productArtifact = new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, - targetArtifactId, "iar", null, true, null); - - metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); - metaProductArtifact.setGroupId(targetGroupId); - metaProductArtifact.setArtifactId(targetArtifactId); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) - .thenReturn(null); - Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); - - metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) - .thenReturn(mockContent); - - try { - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); - Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); - - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)) - .thenThrow(new IOException("Mock IO Exception")); - Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); - } catch (IOException e) { - Fail.fail("Mock setup should not throw an exception"); - } - } - - @Test - void testConvertMavenArtifactToModel() { - String downloadUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; - String artifactName = "Adobe Acrobat Sign Connector (iar)"; - - MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", - "adobe-acrobat-sign-connector", null, null, null, null); - - // Assert case handle artifact without name - MavenArtifactModel result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); - MavenArtifactModel expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); - Assertions.assertEquals(expectedResult.getName(), result.getName()); - Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); - - // Assert case handle artifact with name - artifactName = "Adobe Connector"; - String expectedArtifactName = "Adobe Connector (iar)"; - targetArtifact.setName(artifactName); - result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); - expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); - Assertions.assertEquals(expectedArtifactName, result.getName()); - Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); - } - - @Test - void testConvertMavenArtifactsToModels() { - // Assert case param is empty - List result = versionService.convertMavenArtifactsToModels(Collections.emptyList(), - "10.0.21"); - Assertions.assertEquals(Collections.emptyList(), result); - - // Assert case param is null - result = versionService.convertMavenArtifactsToModels(null, "10.0.21"); - Assertions.assertEquals(Collections.emptyList(), result); - - // Assert case param is a list with existed element - MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", - "adobe-acrobat-sign-connector", null, null, null, null); - result = versionService.convertMavenArtifactsToModels(List.of(targetArtifact), "10.0.21"); - Assertions.assertEquals(1, result.size()); - } - - @Test - void testBuildDownloadUrlFromArtifactAndVersion() { - // Set up artifact for testing - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetGroupId = "com.axonivy.connector"; - MavenArtifact targetArtifact = new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, - null); - String targetVersion = "10.0.10"; - - // Assert case without archived artifact - String expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, - MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, "com/axonivy/connector", targetArtifactId, targetVersion, - targetArtifactId, targetVersion, "iar"); - String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); - Assertions.assertEquals(expectedResult, result); - - // Assert case with artifact not match & use custom repo - ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", - "adobe-connector"); - ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", - "com.axonivy.adobe.sign.connector", "adobe-sign-connector"); - archivedArtifactsMap.put(targetArtifactId, - List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); - String customRepoUrl = "https://nexus.axonivy.com"; - targetArtifact.setRepoUrl(customRepoUrl); - result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); - expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, - "com/axonivy/connector", targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); - Assertions.assertEquals(expectedResult, result); - - // Assert case with artifact got matching archived artifact & use custom file - // type - String customType = "zip"; - targetArtifact.setType(customType); - targetVersion = "10.0.9"; - result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, "10.0.9"); - expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, - "com/axonivy/adobe/connector", "adobe-connector", targetVersion, "adobe-connector", targetVersion, - customType); - Assertions.assertEquals(expectedResult, result); - } - - @Test - void testFindArchivedArtifactInfoBestMatchWithVersion() { - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetVersion = "10.0.10"; - ArchivedArtifact result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, - targetVersion); - Assertions.assertNull(result); - - // Assert case with target version higher than all of latest version from - // archived artifact list - ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.connector", - "adobe-sign-connector"); - ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.connector", - "adobe-acrobat-sign-connector"); - List archivedArtifacts = new ArrayList<>(); - archivedArtifacts.add(adobeArchivedArtifactVersion8); - archivedArtifacts.add(adobeArchivedArtifactVersion9); - archivedArtifactsMap.put(targetArtifactId, archivedArtifacts); - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); - Assertions.assertNull(result); - - // Assert case with target version less than all of latest version from archived - // artifact list - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, "10.0.7"); - Assertions.assertEquals(adobeArchivedArtifactVersion8, result); - - // Assert case with target version is in range of archived artifact list - ArchivedArtifact adobeArchivedArtifactVersion10 = new ArchivedArtifact("10.0.10", "com.axonivy.connector", - "adobe-sign-connector"); - - archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); - Assertions.assertEquals(adobeArchivedArtifactVersion10, result); - } - - @Test - void testConvertArtifactIdToName() { - String defaultArtifactId = "adobe-acrobat-sign-connector"; - String result = versionService.convertArtifactIdToName(defaultArtifactId); - Assertions.assertEquals("Adobe Acrobat Sign Connector", result); - - result = versionService.convertArtifactIdToName(null); - Assertions.assertEquals(StringUtils.EMPTY, result); - - result = versionService.convertArtifactIdToName(StringUtils.EMPTY); - Assertions.assertEquals(StringUtils.EMPTY, result); - - result = versionService.convertArtifactIdToName(" "); - Assertions.assertEquals(StringUtils.EMPTY, result); - } - - @Test - void testGetRepoNameFromMarketRepo() { - String defaultRepositoryName = "market/adobe-acrobat-connector"; - String expectedRepoName = "adobe-acrobat-connector"; - String result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - - defaultRepositoryName = "market/utils/adobe-acrobat-connector"; - result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - - defaultRepositoryName = "adobe-acrobat-connector"; - result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - } - - @Test - void testBuildProductJsonFilePath() { - String version = "10.0.1"; - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - Assertions.assertEquals("v10.0.1", versionService.buildProductJsonFilePath(version)); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.PORTAL); - Assertions.assertEquals("10.0.1", versionService.buildProductJsonFilePath(version)); - Assertions.assertEquals("AxonIvyPortal/portal-product/product.json", versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", - NonStandardProductPackageConstants.CONNECTIVITY_FEATURE); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("connectivity/connectivity-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.ERROR_HANDLING); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("error-handling/error-handling-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.WORKFLOW_DEMO); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("workflow/workflow-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.MICROSOFT_365); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("msgraph-connector-product/products/msgraph-connector/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.HTML_DIALOG_DEMO); - versionService.buildProductJsonFilePath(version); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("html-dialog/html-dialog-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.RULE_ENGINE_DEMOS); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("rule-engine/rule-engine-demos-product/product.json", - versionService.getProductJsonFilePath()); - } -} \ No newline at end of file + private String repoName; + private Map> archivedArtifactsMap; + private List artifactsFromMeta; + private MavenArtifactVersion proceedDataCache; + private MavenArtifact metaProductArtifact; + @Spy + @InjectMocks + private VersionServiceImpl versionService; + + @Mock + private GHAxonIvyProductRepoService gitHubService; + + @Mock + private MavenArtifactVersionRepository mavenArtifactVersionRepository; + + @Mock + private ProductRepository productRepository; + + @BeforeEach() + void prepareBeforeTest() { + archivedArtifactsMap = new HashMap<>(); + artifactsFromMeta = new ArrayList<>(); + metaProductArtifact = new MavenArtifact(); + proceedDataCache = new MavenArtifactVersion(); + repoName = StringUtils.EMPTY; + ReflectionTestUtils.setField(versionService, "archivedArtifactsMap", archivedArtifactsMap); + ReflectionTestUtils.setField(versionService, "artifactsFromMeta", artifactsFromMeta); + ReflectionTestUtils.setField(versionService, "proceedDataCache", proceedDataCache); + ReflectionTestUtils.setField(versionService, "metaProductArtifact", metaProductArtifact); + } + + private void setUpArtifactFromMeta() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + metaProductArtifact.setGroupId(groupId); + metaProductArtifact.setArtifactId(artifactId); + metaProductArtifact.setIsProductArtifact(true); + MavenArtifact additionalMavenArtifact = new MavenArtifact(repoUrl, "", groupId, artifactId, "", null, null, null); + artifactsFromMeta.add(metaProductArtifact); + artifactsFromMeta.add(additionalMavenArtifact); + } + + @Test + void testGetArtifactsAndVersionToDisplay() { + String productId = "adobe-acrobat-sign-connector"; + String targetVersion = "10.0.10"; + setUpArtifactFromMeta(); + when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); + when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())) + .thenReturn(List.of(targetVersion)); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); + ArrayList artifactsInVersion = new ArrayList<>(); + artifactsInVersion.add(new MavenArtifactModel()); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(artifactsInVersion); + Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + + MavenArtifactVersion proceededData = new MavenArtifactVersion(); + proceededData.getProductArtifactWithVersionReleased().put(targetVersion, new ArrayList<>()); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); + Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + } + + @Test + void testHandleArtifactForVersionToDisplay() { + String newVersionDetected = "10.0.10"; + List result = new ArrayList<>(); + List versionsToDisplay = List.of(newVersionDetected); + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + Assertions.assertTrue(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(newVersionDetected, result.get(0).getVersion()); + Assertions.assertEquals(0, result.get(0).getArtifactsByVersion().size()); + + result = new ArrayList<>(); + ArrayList artifactsInVersion = new ArrayList<>(); + artifactsInVersion.add(new MavenArtifactModel()); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(artifactsInVersion); + Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); + } + + @Test + void testGetProductMetaArtifacts() { + Product product = new Product(); + MavenArtifact artifact1 = new MavenArtifact(); + MavenArtifact artifact2 = new MavenArtifact(); + List artifacts = List.of(artifact1, artifact2); + product.setArtifacts(artifacts); + when(productRepository.findById(Mockito.anyString())).thenReturn(Optional.of(product)); + List result = versionService.getProductMetaArtifacts("portal"); + Assertions.assertEquals(artifacts, result); + Assertions.assertNull(versionService.getRepoName()); + + product.setRepositoryName("/market/portal"); + versionService.getProductMetaArtifacts("portal"); + Assertions.assertEquals("portal", versionService.getRepoName()); + } + + @Test + void testUpdateArtifactsInVersionWithProductArtifact() { + String version = "10.0.10"; + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + MavenArtifactModel artifactModel = new MavenArtifactModel(); + List mockMavenArtifactModels = List.of(artifactModel); + when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(mockMavenArtifactModels); + Assertions.assertEquals(mockMavenArtifactModels, + versionService.updateArtifactsInVersionWithProductArtifact(version)); + Assertions.assertEquals(1, proceedDataCache.getVersions().size()); + Assertions.assertEquals(1, proceedDataCache.getProductArtifactWithVersionReleased().size()); + Assertions.assertEquals(version, proceedDataCache.getVersions().get(0)); + } + + @Test + void testSanitizeMetaArtifactBeforeHandle() { + setUpArtifactFromMeta(); + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String archivedArtifactId1 = "adobe-acrobat-sign-connector"; + String archivedArtifactId2 = "adobe-acrobat-sign-connector"; + ArchivedArtifact archivedArtifact1 = new ArchivedArtifact("10.0.10", groupId, archivedArtifactId1); + ArchivedArtifact archivedArtifact2 = new ArchivedArtifact("10.0.20", groupId, archivedArtifactId2); + artifactsFromMeta.get(1).setArchivedArtifacts(List.of(archivedArtifact2, archivedArtifact1)); + + versionService.sanitizeMetaArtifactBeforeHandle(); + String artifactId = "adobe-acrobat-sign-connector"; + + Assertions.assertEquals(1, artifactsFromMeta.size()); + Assertions.assertEquals(1, archivedArtifactsMap.size()); + Assertions.assertEquals(2, archivedArtifactsMap.get(artifactId).size()); + Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); + } + + @Test + void testGetVersionsToDisplay() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + versionFromArtifact.add("10.0.3-SNAPSHOT"); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)).thenReturn(versionFromArtifact); + Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(true, null)); + Assertions.assertEquals(List.of("10.0.5"), versionService.getVersionsToDisplay(null, "10.0.5")); + versionFromArtifact.remove("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(null, null)); + } + + @Test + void getVersionsFromMavenArtifacts() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + String archivedArtifactId = "adobe-sign-connector"; + artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)).thenReturn(versionFromArtifact); + Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); + + List archivedArtifacts = List.of(new ArchivedArtifact("10.0.9", groupId, archivedArtifactId)); + ArrayList versionFromArchivedArtifact = new ArrayList<>(); + versionFromArchivedArtifact.add("10.0.3"); + versionFromArchivedArtifact.add("10.0.2"); + versionFromArchivedArtifact.add("10.0.1"); + artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)) + .thenReturn(versionFromArchivedArtifact); + versionFromArtifact.addAll(versionFromArchivedArtifact); + Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); + } + + @Test + void testGetVersionsFromArtifactDetails() { + + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.16"); + versionFromArtifact.add("10.0.18"); + versionFromArtifact.add("10.0.19"); + versionFromArtifact.add("10.0.20"); + versionFromArtifact.add("10.0.21"); + + try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { + xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); + } + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), + versionFromArtifact); + } + + @Test + void testBuildMavenMetadataUrlFromArtifact() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + String metadataUrl = + "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; + Assertions.assertEquals(StringUtils.EMPTY, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); + Assertions.assertEquals(StringUtils.EMPTY, versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), + StringUtils.EMPTY); + Assertions.assertEquals(metadataUrl, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); + } + + @Test + void testIsReleasedVersionOrUnReleaseDevVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + String unreleasedSprintVersion = "10.0.21-m1235"; + List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); + Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); + Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); + } + + @Test + void testGetBugfixVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(releasedVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(snapshotVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(sprintVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(minorSprintVersion)); + } + + @Test + void testIsSnapshotVersion() { + String targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertTrue(versionService.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21-m1234"; + Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); + } + + @Test + void testIsSprintVersion() { + String targetVersion = "10.0.21-m1234"; + Assertions.assertTrue(versionService.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); + } + + @Test + void testIsReleasedVersion() { + String targetVersion = "10.0.21"; + Assertions.assertTrue(versionService.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-m1231"; + Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); + } + + @Test + void testIsMatchWithDesignerVersion() { + String designerVersion = "10.0.21"; + String targetVersion = "10.0.21.2"; + Assertions.assertTrue(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.19"; + Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + } + + @Test + void testGetProductJsonByVersion() { + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector.adobe.acrobat"; + GHContent mockContent = mock(GHContent.class); + repoName = "adobe-acrobat-sign-connector"; + ReflectionTestUtils.setField(versionService, "repoName", repoName); + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + MavenArtifact productArtifact = + new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, targetArtifactId, "iar", null, true, null); + + metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); + metaProductArtifact.setGroupId(targetGroupId); + metaProductArtifact.setArtifactId(targetArtifactId); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(null); + Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); + + metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockContent); + + try { + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); + Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); + + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)) + .thenThrow(new IOException("Mock IO Exception")); + Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); + } catch (IOException e) { + Fail.fail("Mock setup should not throw an exception"); + } + } + + @Test + void testConvertMavenArtifactToModel() { + String downloadUrl = + "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; + String artifactName = "Adobe Acrobat Sign Connector (iar)"; + + MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null); + + // Assert case handle artifact without name + MavenArtifactModel result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); + MavenArtifactModel expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); + Assertions.assertEquals(expectedResult.getName(), result.getName()); + Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); + + // Assert case handle artifact with name + artifactName = "Adobe Connector"; + String expectedArtifactName = "Adobe Connector (iar)"; + targetArtifact.setName(artifactName); + result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); + expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); + Assertions.assertEquals(expectedArtifactName, result.getName()); + Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); + } + + @Test + void testConvertMavenArtifactsToModels() { + // Assert case param is empty + List result = versionService.convertMavenArtifactsToModels(Collections.emptyList(), "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is null + result = versionService.convertMavenArtifactsToModels(null, "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is a list with existed element + MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null); + result = versionService.convertMavenArtifactsToModels(List.of(targetArtifact), "10.0.21"); + Assertions.assertEquals(1, result.size()); + } + + @Test + void testBuildDownloadUrlFromArtifactAndVersion() { + // Set up artifact for testing + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector"; + MavenArtifact targetArtifact = + new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, null); + String targetVersion = "10.0.10"; + + // Assert case without archived artifact + String expectedResult = + String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, + "com/axonivy/connector", targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); + String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact not match & use custom repo + ArchivedArtifact adobeArchivedArtifactVersion9 = + new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", "adobe-connector"); + ArchivedArtifact adobeArchivedArtifactVersion8 = + new ArchivedArtifact("10.0.8", "com.axonivy.adobe.sign.connector", "adobe-sign-connector"); + archivedArtifactsMap.put(targetArtifactId, List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); + String customRepoUrl = "https://nexus.axonivy.com"; + targetArtifact.setRepoUrl(customRepoUrl); + result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); + expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, "com/axonivy/connector", + targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact got matching archived artifact & use custom file + // type + String customType = "zip"; + targetArtifact.setType(customType); + targetVersion = "10.0.9"; + result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, "10.0.9"); + expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, + "com/axonivy/adobe/connector", "adobe-connector", targetVersion, "adobe-connector", targetVersion, customType); + Assertions.assertEquals(expectedResult, result); + } + + @Test + void testFindArchivedArtifactInfoBestMatchWithVersion() { + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetVersion = "10.0.10"; + ArchivedArtifact result = + versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertNull(result); + + // Assert case with target version higher than all of latest version from + // archived artifact list + ArchivedArtifact adobeArchivedArtifactVersion8 = + new ArchivedArtifact("10.0.8", "com.axonivy.connector", "adobe-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion9 = + new ArchivedArtifact("10.0.9", "com.axonivy.connector", "adobe-acrobat-sign-connector"); + List archivedArtifacts = new ArrayList<>(); + archivedArtifacts.add(adobeArchivedArtifactVersion8); + archivedArtifacts.add(adobeArchivedArtifactVersion9); + archivedArtifactsMap.put(targetArtifactId, archivedArtifacts); + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertNull(result); + + // Assert case with target version less than all of latest version from archived + // artifact list + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, "10.0.7"); + Assertions.assertEquals(adobeArchivedArtifactVersion8, result); + + // Assert case with target version is in range of archived artifact list + ArchivedArtifact adobeArchivedArtifactVersion10 = + new ArchivedArtifact("10.0.10", "com.axonivy.connector", "adobe-sign-connector"); + + archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertEquals(adobeArchivedArtifactVersion10, result); + } + + @Test + void testGetRepoNameFromMarketRepo() { + String defaultRepositoryName = "market/adobe-acrobat-connector"; + String expectedRepoName = "adobe-acrobat-connector"; + String result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + + defaultRepositoryName = "market/utils/adobe-acrobat-connector"; + result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + + defaultRepositoryName = "adobe-acrobat-connector"; + result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java new file mode 100644 index 000000000..9ffc48274 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java @@ -0,0 +1,93 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.github.util.GitHubUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GitHubUtilsTest { + private static final String JIRA_CONNECTOR = "Jira Connector"; + + @Test + void testConvertArtifactIdToName() { + String defaultArtifactId = "adobe-acrobat-sign-connector"; + String result = GitHubUtils.convertArtifactIdToName(defaultArtifactId); + Assertions.assertEquals("Adobe Acrobat Sign Connector", result); + + result = GitHubUtils.convertArtifactIdToName(null); + Assertions.assertEquals(StringUtils.EMPTY, result); + + result = GitHubUtils.convertArtifactIdToName(StringUtils.EMPTY); + Assertions.assertEquals(StringUtils.EMPTY, result); + + result = GitHubUtils.convertArtifactIdToName(" "); + Assertions.assertEquals(StringUtils.EMPTY, result); + } + + @Test + void testBuildProductJsonFilePath() { + String result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.PORTAL); + Assertions.assertEquals("AxonIvyPortal/portal-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.CONNECTIVITY_FEATURE); + Assertions.assertEquals("connectivity/connectivity-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.ERROR_HANDLING); + Assertions.assertEquals("error-handling/error-handling-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.WORKFLOW_DEMO); + Assertions.assertEquals("workflow/workflow-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_365); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-connector", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_CALENDAR); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-calendar", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_TEAMS); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-chat", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_MAIL); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-mail", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_TODO); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-todo", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.HTML_DIALOG_DEMO); + Assertions.assertEquals("html-dialog/html-dialog-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.RULE_ENGINE_DEMOS); + Assertions.assertEquals("rule-engine/rule-engine-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.OPENAI_CONNECTOR); + Assertions.assertEquals("openai-connector-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.OPENAI_ASSISTANT); + Assertions.assertEquals("openai-assistant-product", result); + } + + @Test + void testGetNonStandardImageFolder() { + String result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.EXCEL_IMPORTER); + Assertions.assertEquals("doc", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.EXPRESS_IMPORTER); + Assertions.assertEquals("img", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.DEEPL_CONNECTOR); + Assertions.assertEquals("img", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.GRAPHQL_DEMO); + Assertions.assertEquals("assets", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.OPENAI_ASSISTANT); + Assertions.assertEquals("docs", result); + + result = GitHubUtils.getNonStandardImageFolder(JIRA_CONNECTOR); + Assertions.assertEquals("images", result); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java similarity index 58% rename from marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java rename to marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java index 7245ce561..dc755b0ac 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.utils; +package com.axonivy.market.util; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; @@ -12,10 +12,10 @@ @ExtendWith(MockitoExtension.class) class XmlReaderUtilsTest { - @Test - void testExtractVersions() { - List versions = Collections.emptyList(); - XmlReaderUtils.extractVersions(StringUtils.EMPTY, versions); - Assertions.assertTrue(versions.isEmpty()); - } -} \ No newline at end of file + @Test + void testExtractVersions() { + List versions = Collections.emptyList(); + XmlReaderUtils.extractVersions(StringUtils.EMPTY, versions); + Assertions.assertTrue(versions.isEmpty()); + } +} From 27f4c1b769d6b1377feece4e2288fca61ac3765a Mon Sep 17 00:00:00 2001 From: Khanh Nguyen Date: Tue, 16 Jul 2024 12:34:50 +0700 Subject: [PATCH 21/62] Feature/MARP 357 create detail pages for new market website rating --- marketplace-service/pom.xml | 10 + .../market/MarketplaceServiceApplication.java | 6 +- .../assembler/FeedbackModelAssembler.java | 57 ++++++ .../assembler/ProductModelAssembler.java | 11 +- .../config/MarketApiDocumentConfig.java | 10 +- .../config/MarketHeaderInterceptor.java | 8 +- .../axonivy/market/config/MongoConfig.java | 11 +- .../market/constants/EntityConstants.java | 1 + .../market/constants/GitHubConstants.java | 17 ++ .../constants/RequestMappingConstants.java | 1 + .../market/controller/AppController.java | 12 +- .../market/controller/FeedbackController.java | 107 ++++++++++ .../market/controller/OAuth2Controller.java | 48 +++++ .../market/controller/ProductController.java | 44 ++-- .../market/controller/UserController.java | 27 --- .../com/axonivy/market/entity/Feedback.java | 55 +++++ .../axonivy/market/entity/GitHubRepoMeta.java | 7 +- .../market/entity/MavenArtifactModel.java | 9 +- .../java/com/axonivy/market/entity/User.java | 47 ++++- .../com/axonivy/market/enums/ErrorCode.java | 6 +- .../com/axonivy/market/enums/FileStatus.java | 4 +- .../com/axonivy/market/enums/FileType.java | 4 +- .../com/axonivy/market/enums/SortOption.java | 4 +- .../com/axonivy/market/enums/TypeOption.java | 4 +- .../market/exceptions/ExceptionHandlers.java | 44 +++- .../model/InvalidParamException.java | 1 - .../model/MissingHeaderException.java | 3 + .../exceptions/model/NotFoundException.java | 4 +- .../model/Oauth2ExchangeCodeException.java | 19 ++ .../market/github/model/ArchivedArtifact.java | 1 - .../market/github/model/GitHubFile.java | 5 +- .../com/axonivy/market/github/model/Meta.java | 5 +- .../service/GHAxonIvyMarketRepoService.java | 7 +- .../market/github/service/GitHubService.java | 22 +- .../impl/GHAxonIvyMarketRepoServiceImpl.java | 27 +-- .../service/impl/GitHubServiceImpl.java | 89 ++++++++- .../axonivy/market/model/DisplayValue.java | 6 +- .../axonivy/market/model/FeedbackModel.java | 42 ++++ .../market/model/MultilingualismValue.java | 4 +- .../market/model/Oauth2AuthorizationCode.java | 12 ++ .../axonivy/market/model/ProductModel.java | 14 +- .../axonivy/market/model/ProductRating.java | 16 ++ .../market/repository/FeedbackRepository.java | 21 ++ .../repository/GitHubRepoMetaRepository.java | 3 +- .../market/repository/ProductRepository.java | 3 +- .../market/repository/UserRepository.java | 1 + .../market/schedulingtask/ScheduledTasks.java | 6 +- .../market/service/FeedbackService.java | 17 ++ .../axonivy/market/service/JwtService.java | 10 + .../market/service/ProductService.java | 3 +- .../axonivy/market/service/UserService.java | 7 +- .../service/impl/FeedbackServiceImpl.java | 102 ++++++++++ .../market/service/impl/JwtServiceImpl.java | 54 +++++ .../service/impl/ProductServiceImpl.java | 23 +-- .../market/service/impl/UserServiceImpl.java | 18 +- .../src/main/resources/application.properties | 4 + .../market/controller/AppControllerTest.java | 6 +- .../controller/FeedbackControllerTest.java | 161 +++++++++++++++ .../controller/OAuth2ControllerTest.java | 66 ++++++ .../controller/ProductControllerTest.java | 39 ++-- .../ProductDetailsControllerTest.java | 26 ++- .../market/controller/UserControllerTest.java | 27 --- .../market/handler/ExceptionHandlersTest.java | 17 +- .../service/FeedbackServiceImplTest.java | 189 ++++++++++++++++++ .../GHAxonIvyMarketRepoServiceImplTest.java | 31 ++- .../market/service/GitHubServiceImplTest.java | 35 +++- .../market/service/JwtServiceImplTest.java | 94 +++++++++ .../market/service/SchedulingTasksTest.java | 7 +- .../market/service/UserServiceImplTest.java | 27 ++- 69 files changed, 1515 insertions(+), 313 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java delete mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml index 947e700d7..d6a0d9a67 100644 --- a/marketplace-service/pom.xml +++ b/marketplace-service/pom.xml @@ -64,6 +64,16 @@ github-api 1.321 + + io.jsonwebtoken + jjwt + 0.9.1 + + + javax.xml.bind + jaxb-api + 2.3.1 + diff --git a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java index 06660037c..52cdb27d9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java +++ b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -1,5 +1,7 @@ package com.axonivy.market; +import com.axonivy.market.service.ProductService; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.time.StopWatch; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -9,10 +11,6 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; -import com.axonivy.market.service.ProductService; - -import lombok.extern.log4j.Log4j2; - @Log4j2 @EnableAsync @EnableScheduling diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java new file mode 100644 index 000000000..a981b099a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java @@ -0,0 +1,57 @@ +package com.axonivy.market.assembler; + +import com.axonivy.market.controller.FeedbackController; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@Log4j2 +@Component +public class FeedbackModelAssembler extends RepresentationModelAssemblerSupport { + + private final UserService userService; + + public FeedbackModelAssembler(UserService userService) { + super(Feedback.class, FeedbackModel.class); + this.userService = userService; + } + + @Override + public FeedbackModel toModel(Feedback feedback) { + FeedbackModel resource = new FeedbackModel(); + resource.add(linkTo(methodOn(FeedbackController.class).findFeedback(feedback.getId())) + .withSelfRel()); + return createResource(resource, feedback); + } + + private FeedbackModel createResource(FeedbackModel model, Feedback feedback) { + User user; + try { + user = userService.findUser(feedback.getUserId()); + } + catch (NotFoundException e) { + log.warn(e.getMessage()); + user = new User(); + } + model.setId(feedback.getId()); + model.setUsername(StringUtils.isBlank(user.getName()) ? user.getUsername() : user.getName()); + model.setUserAvatarUrl(user.getAvatarUrl()); + model.setUserProvider(user.getProvider()); + model.setProductId(feedback.getProductId()); + model.setContent(feedback.getContent()); + model.setRating(feedback.getRating()); + model.setCreatedAt(feedback.getCreatedAt()); + model.setUpdatedAt(feedback.getUpdatedAt()); + return model; + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java index 00b94a52f..a50d82d1a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -1,14 +1,13 @@ package com.axonivy.market.assembler; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.stereotype.Component; - import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.model.ProductModel; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @Component public class ProductModelAssembler extends RepresentationModelAssemblerSupport { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java index 61b6a0773..805f30120 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -1,15 +1,15 @@ package com.axonivy.market.config; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.parameters.Parameter; -import static com.axonivy.market.constants.CommonConstants.*; +import static com.axonivy.market.constants.CommonConstants.REQUESTED_BY; @Configuration public class MarketApiDocumentConfig { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java index 963706069..83b281062 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java @@ -1,15 +1,13 @@ package com.axonivy.market.config; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.exceptions.model.MissingHeaderException; - import io.swagger.v3.oas.models.PathItem.HttpMethod; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; @Component public class MarketHeaderInterceptor implements HandlerInterceptor { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java index a6cd2bc05..7f558f1cc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -1,10 +1,15 @@ package com.axonivy.market.config; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -12,13 +17,9 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; - @Configuration @EnableMongoRepositories(basePackages = "com.axonivy.market.repository") +@EnableMongoAuditing public class MongoConfig extends AbstractMongoClientConfiguration { @Value("${spring.data.mongodb.host}") diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java index 76c1c45ab..0d9752cb9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -9,4 +9,5 @@ public class EntityConstants { public static final String PRODUCT = "Product"; public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; public static final String GH_REPO_META = "GitHubRepoMeta"; + public static final String FEEDBACK = "Feedback"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java index 33c84df88..39d35a77c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -10,4 +10,21 @@ public class GitHubConstants { public static final String AXONIVY_MARKETPLACE_PATH = "market"; public static final String DEFAULT_BRANCH = "feature/MARP-463-Multilingualism-for-Website"; public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; + public static final String GITHUB_PROVIDER_NAME = "GitHub"; + public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Json { + public static final String ACCESS_TOKEN = "access_token"; + public static final String TOKEN = "token"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String CODE = "code"; + public static final String ERROR = "error"; + public static final String ERROR_DESCRIPTION = "error"; + public static final String USER_ID = "id"; + public static final String USER_NAME = "name"; + public static final String USER_AVATAR_URL = "avatar_url"; + public static final String USER_LOGIN_NAME = "login"; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index f4687a442..5efdc47e6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -11,5 +11,6 @@ public class RequestMappingConstants { public static final String USER_MAPPING = "/user"; public static final String PRODUCT = API + "/product"; public static final String PRODUCT_DETAILS = API + "/product-details"; + public static final String FEEDBACK = API + "/feedback"; public static final String SWAGGER_URL = "/swagger-ui/index.html"; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java index 60523b72a..45f712d2d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java @@ -1,8 +1,8 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.ROOT; -import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; - +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,10 +10,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; - -import lombok.extern.log4j.Log4j2; +import static com.axonivy.market.constants.RequestMappingConstants.ROOT; +import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; @Log4j2 @RestController diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java new file mode 100644 index 000000000..233c94b4e --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -0,0 +1,107 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.JwtService; +import io.jsonwebtoken.Claims; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; + +import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; + +@RestController +@RequestMapping(FEEDBACK) +public class FeedbackController { + + private final FeedbackService feedbackService; + private final JwtService jwtService; + private final FeedbackModelAssembler feedbackModelAssembler; + + private final PagedResourcesAssembler pagedResourcesAssembler; + + public FeedbackController(FeedbackService feedbackService, JwtService jwtService, FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { + this.feedbackService = feedbackService; + this.jwtService = jwtService; + this.feedbackModelAssembler = feedbackModelAssembler; + this.pagedResourcesAssembler = pagedResourcesAssembler; + } + + @Operation(summary = "Find all feedbacks by product id") + @GetMapping("/product/{productId}") + public ResponseEntity> findFeedbacks(@PathVariable("productId") String productId, Pageable pageable) { + Page results = feedbackService.findFeedbacks(productId, pageable); + if (results.isEmpty()) { + return generateEmptyPagedModel(); + } + var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements()); + var pageResources = pagedResourcesAssembler.toModel(responseContent, feedbackModelAssembler); + return new ResponseEntity<>(pageResources, HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity findFeedback(@PathVariable("id") String id) { + Feedback feedback = feedbackService.findFeedback(id); + return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); + } + + @Operation(summary = "Find all feedbacks by user id and product id") + @GetMapping() + public ResponseEntity findFeedbackByUserIdAndProductId( + @RequestParam String userId, + @RequestParam String productId) { + Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); + return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); + } + + @PostMapping + public ResponseEntity createFeedback(@RequestBody @Valid Feedback feedback, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { + String token = null; + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + token = authorizationHeader.substring(7); // Remove "Bearer " prefix + } + + // Validate the token + if (token == null || !jwtService.validateToken(token)) { + return ResponseEntity.status(401).build(); // Unauthorized if token is missing or invalid + } + + Claims claims = jwtService.getClaimsFromToken(token); + feedback.setUserId(claims.getSubject()); + Feedback newFeedback = feedbackService.upsertFeedback(feedback); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newFeedback.getId()) + .toUri(); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "Find rating information of product by id") + @GetMapping("/product/{productId}/rating") + public ResponseEntity> getProductRating(@PathVariable("productId") String productId) { + return ResponseEntity.ok(feedbackService.getProductRatingById(productId)); + } + + @SuppressWarnings("unchecked") + private ResponseEntity> generateEmptyPagedModel() { + var emptyPagedModel = (PagedModel) pagedResourcesAssembler + .toEmptyModel(Page.empty(), FeedbackModel.class); + return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java new file mode 100644 index 000000000..a3ebbca64 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -0,0 +1,48 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +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.RestController; + +import java.util.Collections; +import java.util.Map; + +@RestController +@RequestMapping("/auth") +public class OAuth2Controller { + + @Value("${spring.security.oauth2.client.registration.github.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.github.client-secret}") + private String clientSecret; + + private final GitHubService gitHubService; + + private final JwtService jwtService; + + public OAuth2Controller(GitHubService gitHubService, JwtService jwtService) { + this.gitHubService = gitHubService; + this.jwtService = jwtService; + } + + @PostMapping("/github/login") + public ResponseEntity gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { + Map tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, clientSecret); + String accessToken = (String) tokenResponse.get(GitHubConstants.Json.ACCESS_TOKEN); + + User user = gitHubService.getAndUpdateUser(accessToken); + + String jwtToken = jwtService.generateToken(user); + + return ResponseEntity.ok().body(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken)); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index ce273cd33..6dbd73c4d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -1,8 +1,12 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; - +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import com.axonivy.market.model.ProductModel; +import com.axonivy.market.service.ProductService; +import io.swagger.v3.oas.annotations.Operation; import org.apache.commons.lang3.time.StopWatch; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -11,32 +15,22 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; -import com.axonivy.market.model.ProductModel; -import com.axonivy.market.service.ProductService; - -import io.swagger.v3.oas.annotations.Operation; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; @RestController @RequestMapping(PRODUCT) public class ProductController { - private final ProductService service; + private final ProductService productService; private final ProductModelAssembler assembler; private final PagedResourcesAssembler pagedResourcesAssembler; - public ProductController(ProductService service, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { - this.service = service; + public ProductController(ProductService productService, ProductModelAssembler assembler, + PagedResourcesAssembler pagedResourcesAssembler) { + this.productService = productService; this.assembler = assembler; this.pagedResourcesAssembler = pagedResourcesAssembler; } @@ -44,14 +38,14 @@ public ProductController(ProductService service, ProductModelAssembler assembler @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() public ResponseEntity> findProducts( - @RequestParam(required = true, name = "type") String type, + @RequestParam(name = "type") String type, @RequestParam(required = false, name = "keyword") String keyword, - @RequestParam(required = true, name = "language") String language, Pageable pageable) { - Page results = service.findProducts(type, keyword, language, pageable); + @RequestParam(name = "language") String language, Pageable pageable) { + Page results = productService.findProducts(type, keyword, language, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); } - var responseContent = new PageImpl(results.getContent(), pageable, results.getTotalElements()); + var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements()); var pageResources = pagedResourcesAssembler.toModel(responseContent, assembler); return new ResponseEntity<>(pageResources, HttpStatus.OK); } @@ -60,7 +54,7 @@ public ResponseEntity> findProducts( public ResponseEntity syncProducts() { var stopWatch = new StopWatch(); stopWatch.start(); - var isAlreadyUpToDate = service.syncLatestDataFromMarketRepo(); + var isAlreadyUpToDate = productService.syncLatestDataFromMarketRepo(); var message = new Message(); message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java deleted file mode 100644 index c83c7cc2b..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.axonivy.market.controller; - -import com.axonivy.market.entity.User; -import com.axonivy.market.service.UserService; -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 java.util.List; - -import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; - -@RestController -@RequestMapping(USER_MAPPING) -public class UserController { - private final UserService userService; - - public UserController(UserService userService) { - this.userService = userService; - } - - @GetMapping - public ResponseEntity> getAllUser() { - return ResponseEntity.ok(userService.getAllUsers()); - } -} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java new file mode 100644 index 000000000..166da0c23 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java @@ -0,0 +1,55 @@ +package com.axonivy.market.entity; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +import static com.axonivy.market.constants.EntityConstants.FEEDBACK; + +@Getter +@Setter +@NoArgsConstructor +@Document(FEEDBACK) +public class Feedback implements Serializable { + + @Serial + private static final long serialVersionUID = 29519800556564714L; + + @Id + private String id; + + private String userId; + + @NotBlank(message = "Product id cannot be blank") + private String productId; + + @NotBlank(message = "Content cannot be blank") + @Size(max = 5, message = "Content length must be up to 250 characters") + private String content; + + @Min(value = 1, message = "Rating should not be less than 1") + @Max(value = 5, message = "Rating should not be greater than 5") + private Integer rating; + + @CreatedDate + private Date createdAt; + + @LastModifiedDate + private Date updatedAt; + + public void setContent(String content) { + this.content = content != null ? content.trim() : null; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java index 2e0770816..d2ef46fbf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java @@ -1,12 +1,11 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; - +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.Getter; -import lombok.Setter; +import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java index 6b5328e26..2d48d4c6a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java @@ -1,14 +1,13 @@ package com.axonivy.market.entity; -import java.io.Serializable; -import java.util.Objects; - -import org.springframework.data.annotation.Transient; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.annotation.Transient; + +import java.io.Serializable; +import java.util.Objects; @AllArgsConstructor @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java index 1b88f1095..0f8e7b612 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java @@ -1,21 +1,48 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.USER; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; + +import static com.axonivy.market.constants.EntityConstants.USER; @Getter @Setter @NoArgsConstructor @Document(USER) -public class User { - @Id - private String id; - private String username; - private String password; +public class User implements Serializable { + @Serial + private static final long serialVersionUID = -1244486023332931059L; + + @Id + private String id; + + @Indexed(unique = true) + private String gitHubId; + + private String provider; + private String username; + private String name; + private String avatarUrl; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((User) obj).getId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index de9d54c28..7aef2b47c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -18,8 +18,12 @@ public enum ErrorCode { SUCCESSFUL("0000", "SUCCESSFUL"), PRODUCT_FILTER_INVALID("1101", "PRODUCT_FILTER_INVALID"), PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"), + PRODUCT_NOT_FOUND("1103", "PRODUCT_NOT_FOUND"), GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), - GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"); + GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), + USER_NOT_FOUND("2103", "USER_NOT_FOUND"), + FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), + ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); String code; String helpText; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java index d75ca9f54..eb155031f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.NotFoundException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java index 75bb5beb9..7703e3cbd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.NotFoundException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java index 59914ab90..c3e9714e3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.InvalidParamException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java index 3b513ea4a..1c30aca92 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java @@ -1,10 +1,8 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.InvalidParamException; - import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter public enum TypeOption { diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index 3dc315608..d9b1ab725 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -1,19 +1,47 @@ package com.axonivy.market.exceptions; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.model.Message; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.Message; +import java.util.ArrayList; +import java.util.List; @ControllerAdvice public class ExceptionHandlers extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + BindingResult bindingResult = ex.getBindingResult(); + List errors = new ArrayList<>(); + if (bindingResult.hasErrors()) { + for (FieldError fieldError : bindingResult.getFieldErrors()) { + errors.add(fieldError.getDefaultMessage()); + } + } else { + errors.add(ex.getMessage()); + } + + var errorMessage = new Message(); + errorMessage.setHelpCode(ErrorCode.ARGUMENT_BAD_REQUEST.getCode()); + errorMessage.setMessageDetails(ErrorCode.ARGUMENT_BAD_REQUEST.getHelpText() + " - " + String.join("; ", errors)); + return new ResponseEntity<>(errorMessage, status); + } + @ExceptionHandler(MissingHeaderException.class) public ResponseEntity handleMissingServletRequestParameter(MissingHeaderException missingHeaderException) { var errorMessage = new Message(); @@ -36,4 +64,12 @@ public ResponseEntity handleInvalidException(InvalidParamException inval errorMessage.setMessageDetails(invalidDataException.getMessage()); return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(Oauth2ExchangeCodeException.class) + public ResponseEntity handleOauth2ExchangeCodeException(Oauth2ExchangeCodeException oauth2ExchangeCodeException) { + var errorMessage = new Message(); + errorMessage.setHelpCode(oauth2ExchangeCodeException.getError()); + errorMessage.setMessageDetails(oauth2ExchangeCodeException.getErrorDescription()); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java index 8a82188fa..3cecdf03e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java @@ -1,7 +1,6 @@ package com.axonivy.market.exceptions.model; import com.axonivy.market.enums.ErrorCode; - import lombok.Getter; import lombok.Setter; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java index 4b5b158c6..2b6978f7f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java @@ -1,9 +1,12 @@ package com.axonivy.market.exceptions.model; +import java.io.Serial; + import static com.axonivy.market.constants.ErrorMessageConstants.INVALID_MISSING_HEADER_ERROR_MESSAGE; public class MissingHeaderException extends Exception { + @Serial private static final long serialVersionUID = 1L; public MissingHeaderException() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java index e1c917749..e84dd9c01 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java @@ -1,16 +1,18 @@ package com.axonivy.market.exceptions.model; import com.axonivy.market.enums.ErrorCode; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import java.io.Serial; + @Getter @Setter @AllArgsConstructor public class NotFoundException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; private static final String SEPARATOR = "-"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java new file mode 100644 index 000000000..d48a88770 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java @@ -0,0 +1,19 @@ +package com.axonivy.market.exceptions.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; + +@Getter +@Setter +@AllArgsConstructor +public class Oauth2ExchangeCodeException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 6778659816121728814L; + + private String error; + private String errorDescription; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java index 1bde19a0e..f9ff5a69f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java index 9586f0886..ca5d5ba70 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java @@ -1,14 +1,13 @@ package com.axonivy.market.github.model; -import java.util.Date; - import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Date; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java index 92e940487..918b90378 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java @@ -1,16 +1,15 @@ package com.axonivy.market.github.model; -import java.util.List; - import com.axonivy.market.model.DisplayValue; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.List; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java index a669fac2a..8a3cb88f3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java @@ -1,13 +1,12 @@ package com.axonivy.market.github.service; -import java.util.List; -import java.util.Map; - +import com.axonivy.market.github.model.GitHubFile; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; -import com.axonivy.market.github.model.GitHubFile; +import java.util.List; +import java.util.Map; public interface GHAxonIvyMarketRepoService { diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index 7dd5009db..5cf1a9d01 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -1,22 +1,28 @@ package com.axonivy.market.github.service; -import java.io.IOException; -import java.util.List; - +import com.axonivy.market.entity.User; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +import java.io.IOException; +import java.util.List; +import java.util.Map; + public interface GitHubService { - public GitHub getGitHub() throws IOException; + GitHub getGitHub() throws IOException; + + GHOrganization getOrganization(String orgName) throws IOException; + + GHRepository getRepository(String repositoryPath) throws IOException; - public GHOrganization getOrganization(String orgName) throws IOException; + List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; - public GHRepository getRepository(String repositoryPath) throws IOException; + GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; - public List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; + Map getAccessToken(String code, String clientId, String clientSecret); - public GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; + User getAndUpdateUser(String accessToken); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index 6c96bee26..61aca55a9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -1,21 +1,5 @@ package com.axonivy.market.github.service.impl; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHCommitQueryBuilder; -import org.kohsuke.github.GHCompare; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.springframework.stereotype.Service; - import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; @@ -23,8 +7,17 @@ import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; - import lombok.extern.log4j.Log4j2; +import org.kohsuke.github.*; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Log4j2 @Service diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index 62bbd9181..6af97d1fa 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -1,26 +1,40 @@ package com.axonivy.market.github.service.impl; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.List; - -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GitHubBuilder; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.repository.UserRepository; +import org.kohsuke.github.*; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.ResourceUtils; +import org.springframework.web.client.RestTemplate; -import com.axonivy.market.github.service.GitHubService; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.Map; @Service public class GitHubServiceImpl implements GitHubService { + private final RestTemplate restTemplate; + private final UserRepository userRepository; + private static final String GITHUB_TOKEN_FILE = "classpath:github.token"; + public GitHubServiceImpl(RestTemplateBuilder restTemplateBuilder, UserRepository userRepository) { + this.restTemplate = restTemplateBuilder.build(); + this.userRepository = userRepository; + } + @Override public GitHub getGitHub() throws IOException { File gitHubToken = ResourceUtils.getFile(GITHUB_TOKEN_FILE); @@ -49,4 +63,57 @@ public GHContent getGHContent(GHRepository ghRepository, String path, String ref Assert.notNull(ghRepository, "Repository must not be null"); return ghRepository.getFileContent(path, ref); } + + @Override + public Map getAccessToken(String code, String clientId, String clientSecret) throws Oauth2ExchangeCodeException { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(GitHubConstants.Json.CLIENT_ID, clientId); + params.add(GitHubConstants.Json.CLIENT_SECRET, clientSecret); + params.add(GitHubConstants.Json.CODE, code); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.postForEntity(GitHubConstants.GITHUB_GET_ACCESS_TOKEN_URL, request, Map.class); + if (response.getBody().containsKey(GitHubConstants.Json.ERROR)) { + throw new Oauth2ExchangeCodeException(response.getBody().get(GitHubConstants.Json.ERROR).toString(), response.getBody().get(GitHubConstants.Json.ERROR_DESCRIPTION).toString()); + } + return response.getBody(); + } + + @Override + public User getAndUpdateUser(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + "https://api.github.com/user", HttpMethod.GET, entity, Map.class); + + Map userDetails = response.getBody(); + + if (userDetails == null) { + throw new RuntimeException("Failed to fetch user details from GitHub"); + } + + String gitHubId = userDetails.get(GitHubConstants.Json.USER_ID).toString(); + String name = (String) userDetails.get(GitHubConstants.Json.USER_NAME); + String avatarUrl = (String) userDetails.get(GitHubConstants.Json.USER_AVATAR_URL); + String username = (String) userDetails.get(GitHubConstants.Json.USER_LOGIN_NAME); + + User user = userRepository.searchByGitHubId(gitHubId); + if (user == null) { + user = new User(); + } + user.setGitHubId(gitHubId); + user.setName(name); + user.setUsername(username); + user.setAvatarUrl(avatarUrl); + user.setProvider(GitHubConstants.GITHUB_PROVIDER_NAME); + + userRepository.save(user); + + return user; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java index cd0bf4abf..70d54b588 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java @@ -1,15 +1,13 @@ package com.axonivy.market.model; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java new file mode 100644 index 000000000..5eb1769ce --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java @@ -0,0 +1,42 @@ +package com.axonivy.market.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import java.util.Date; + +@Getter +@Setter +@NoArgsConstructor +@Relation(collectionRelation = "feedbacks", itemRelation = "feedback") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FeedbackModel extends RepresentationModel { + private String id; + private String username; + private String userAvatarUrl; + private String userProvider; + private String productId; + private String content; + private Integer rating; + private Date createdAt; + private Date updatedAt; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((FeedbackModel) obj).getId()).isEquals(); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java index 58431cf8e..389c4832e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java @@ -1,11 +1,11 @@ package com.axonivy.market.model; -import java.io.Serializable; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java new file mode 100644 index 000000000..56706c4c1 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java @@ -0,0 +1,12 @@ +package com.axonivy.market.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Oauth2AuthorizationCode { + public String code; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java index 7ba586438..0984f8765 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java @@ -1,18 +1,16 @@ package com.axonivy.market.model; -import java.util.List; - -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.core.Relation; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import java.util.List; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java new file mode 100644 index 000000000..b151e05f4 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java @@ -0,0 +1,16 @@ +package com.axonivy.market.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ProductRating { + private Integer starRating; + private Integer commentNumber; + private Integer percent; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java new file mode 100644 index 000000000..bbb1fc301 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java @@ -0,0 +1,21 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.Feedback; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FeedbackRepository extends MongoRepository { + + @Query("{ 'productId': ?0 }") + Page searchByProductId(String productId, Pageable pageable); + + List findByProductId(String productId); + + Feedback findByUserIdAndProductId(String userId, String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java index 49424d63c..6dda95549 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java @@ -1,8 +1,7 @@ package com.axonivy.market.repository; -import org.springframework.data.mongodb.repository.MongoRepository; - import com.axonivy.market.entity.GitHubRepoMeta; +import org.springframework.data.mongodb.repository.MongoRepository; public interface GitHubRepoMetaRepository extends MongoRepository { diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index 7fabd79bc..b638e30ea 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -1,13 +1,12 @@ package com.axonivy.market.repository; +import com.axonivy.market.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; -import com.axonivy.market.entity.Product; - import java.util.Optional; @Repository diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java index 6a011e9c6..30969ab97 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java @@ -6,4 +6,5 @@ @Repository public interface UserRepository extends MongoRepository { + User searchByGitHubId(String gitHubId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java index 96153ba39..5621c2d84 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java +++ b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java @@ -1,11 +1,9 @@ package com.axonivy.market.schedulingtask; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - import com.axonivy.market.service.ProductService; - import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; @Log4j2 @Component diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java new file mode 100644 index 000000000..1e8988f22 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java @@ -0,0 +1,17 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface FeedbackService { + Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException; + Feedback findFeedback(String id) throws NotFoundException; + Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException; + Feedback upsertFeedback(Feedback feedback) throws NotFoundException; + List getProductRatingById(String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java new file mode 100644 index 000000000..49f1c9d44 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java @@ -0,0 +1,10 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.User; +import io.jsonwebtoken.Claims; + +public interface JwtService { + String generateToken(User user); + boolean validateToken(String token); + Claims getClaimsFromToken(String token); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index b90604d42..a44f60668 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -1,10 +1,9 @@ package com.axonivy.market.service; +import com.axonivy.market.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import com.axonivy.market.entity.Product; - public interface ProductService { Page findProducts(String type, String keyword, String language, Pageable pageable); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java index 99c08f031..b6c064b4f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java @@ -1,9 +1,12 @@ package com.axonivy.market.service; -import java.util.List; - import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; + +import java.util.List; public interface UserService { List getAllUsers(); + User createUser(User user); + User findUser(String id) throws NotFoundException; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java new file mode 100644 index 000000000..74ae9a998 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -0,0 +1,102 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.repository.FeedbackRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.UserService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +public class FeedbackServiceImpl implements FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final UserRepository userRepository; + private final ProductRepository productRepository; + + public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository userRepository, ProductRepository productRepository, UserService userService) { + this.feedbackRepository = feedbackRepository; + this.userRepository = userRepository; + this.productRepository = productRepository; + } + + @Override + public Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException { + validateProductExists(productId); + return feedbackRepository.searchByProductId(productId, pageable); + } + + @Override + public Feedback findFeedback(String id) throws NotFoundException { + return feedbackRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, "Not found feedback with id: " + id)); + } + + @Override + public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException { + userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + userId)); + validateProductExists(productId); + + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, productId); + if (existingUserFeedback == null) { + throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId)); + } + return existingUserFeedback; + } + + @Override + public Feedback upsertFeedback(Feedback feedback) throws NotFoundException { + userRepository.findById(feedback.getUserId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND,"Not found user with id: " + feedback.getUserId())); + + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(feedback.getUserId(), feedback.getProductId()); + if (existingUserFeedback == null) { + return feedbackRepository.save(feedback); + } else { + existingUserFeedback.setRating(feedback.getRating()); + existingUserFeedback.setContent(feedback.getContent()); + return feedbackRepository.save(existingUserFeedback); + } + } + + @Override + public List getProductRatingById(String productId) { + List feedbacks = feedbackRepository.findByProductId(productId); + int totalFeedbacks = feedbacks.size(); + + if (totalFeedbacks == 0) { + return IntStream.rangeClosed(1, 5) + .mapToObj(star -> new ProductRating(star, 0, 0)) + .collect(Collectors.toList()); + } + + Map ratingCountMap = feedbacks.stream() + .collect(Collectors.groupingBy(Feedback::getRating, Collectors.counting())); + + return IntStream.rangeClosed(1, 5) + .mapToObj(star -> { + long count = ratingCountMap.getOrDefault(star, 0L); + int percent = (int) ((count * 100) / totalFeedbacks); + return new ProductRating(star, Math.toIntExact(count), percent); + }) + .collect(Collectors.toList()); + } + + private void validateProductExists(String productId) throws NotFoundException { + productRepository.findById(productId) + .orElseThrow(() -> new NotFoundException(ErrorCode.PRODUCT_NOT_FOUND, "Not found product with id: " + productId)); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java new file mode 100644 index 000000000..979bec72c --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java @@ -0,0 +1,54 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.User; +import com.axonivy.market.service.JwtService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +public class JwtServiceImpl implements JwtService { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + public String generateToken(User user) { + Map claims = new HashMap<>(); + claims.put("name", user.getName()); + claims.put("username", user.getUsername()); + return Jwts.builder() + .setClaims(claims) + .setSubject(user.getId()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration * 86400000)) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + public boolean validateToken(String token) { + try { + getClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + public Claims getClaimsFromToken(String token) { + return getClaimsJws(token).getBody(); + } + + public Jws getClaimsJws(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 80bfe0645..fd22c680c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -6,10 +6,6 @@ import java.io.IOException; import java.net.URL; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; @@ -264,23 +260,12 @@ private void updateProductFromReleaseTags(Product product) { product.setCompatibility(compatibility); } - List> completableFutures = new ArrayList<>(); - ExecutorService service = Executors.newFixedThreadPool(10); + List productModuleContents = new ArrayList<>(); for (GHTag ghtag : tags) { - completableFutures.add(CompletableFuture.supplyAsync( - () -> axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()), - service)); + ProductModuleContent productModuleContent = + axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()); + productModuleContents.add(productModuleContent); } - completableFutures.forEach(CompletableFuture::join); - List productModuleContents = completableFutures.stream().map(completableFuture -> { - try { - return completableFuture.get(); - } catch (InterruptedException | ExecutionException e) { - Thread.currentThread().interrupt(); - log.error("Get readme and product json contents failed", e); - return null; - } - }).toList(); product.setProductModuleContents(productModuleContents); } catch (Exception e) { log.error("Cannot find repository by path {} {}", product.getRepositoryName(), e); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java index dc9990949..750bf3995 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java @@ -1,12 +1,13 @@ package com.axonivy.market.service.impl; -import java.util.List; - -import org.springframework.stereotype.Service; - import com.axonivy.market.entity.User; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.repository.UserRepository; import com.axonivy.market.service.UserService; +import org.springframework.stereotype.Service; + +import java.util.List; @Service public class UserServiceImpl implements UserService { @@ -22,4 +23,13 @@ public List getAllUsers() { return userRepository.findAll(); } + @Override + public User findUser(String id) throws NotFoundException { + return userRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + id)); + } + + @Override + public User createUser(User user) { + return userRepository.save(user); + } } diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 458102046..878e93e0f 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,3 +9,7 @@ springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] market.cors.allowed.origin.maxAge=3600 +spring.security.oauth2.client.registration.github.client-id= +spring.security.oauth2.client.registration.github.client-secret= +jwt.secret= +jwt.expiration=365 diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java index 104006d9d..055b81d2c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java @@ -1,14 +1,14 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.springframework.http.HttpStatus; import org.springframework.test.context.junit.jupiter.SpringExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + @ExtendWith(SpringExtension.class) class AppControllerTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java new file mode 100644 index 000000000..55817625b --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java @@ -0,0 +1,161 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.JwtService; +import com.axonivy.market.service.UserService; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FeedbackControllerTest { + + private static final String PRODUCT_ID_SAMPLE = "product-id"; + private static final String FEEDBACK_ID_SAMPLE = "feedback-id"; + private static final String USER_ID_SAMPLE = "user-id"; + private static final String USER_NAME_SAMPLE = "Test User"; + private static final String TOKEN_SAMPLE = "token-sample"; + + @Mock + private FeedbackService service; + + @Mock + private JwtService jwtService; + + @Mock + private UserService userService; + + @Mock + private FeedbackModelAssembler feedbackModelAssembler; + + @Mock + private PagedResourcesAssembler pagedResourcesAssembler; + + @InjectMocks + private FeedbackController feedbackController; + + @BeforeEach + void setup() { + feedbackModelAssembler = new FeedbackModelAssembler(userService); + feedbackController = new FeedbackController(service, jwtService, feedbackModelAssembler, pagedResourcesAssembler); + } + + @Test + void testFindFeedbacksAsEmpty() { + PageRequest pageable = PageRequest.of(0, 20); + Page mockFeedbacks = new PageImpl<>(List.of(), pageable, 0); + when(service.findFeedbacks(any(), any())).thenReturn(mockFeedbacks); + when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); + var result = feedbackController.findFeedbacks(PRODUCT_ID_SAMPLE, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(0, result.getBody().getContent().size()); + } + + @Test + void testFindFeedbacks() { + PageRequest pageable = PageRequest.of(0, 20); + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + + Page mockFeedbacks = new PageImpl<>(List.of(mockFeedback), pageable, 1); + when(service.findFeedbacks(any(), any())).thenReturn(mockFeedbacks); + when(userService.findUser(any())).thenReturn(mockUser); + var mockFeedbackModel = feedbackModelAssembler.toModel(mockFeedback); + var mockPagedModel = PagedModel.of(List.of(mockFeedbackModel), new PagedModel.PageMetadata(1, 0, 1)); + when(pagedResourcesAssembler.toModel(any(), any(FeedbackModelAssembler.class))).thenReturn(mockPagedModel); + var result = feedbackController.findFeedbacks(PRODUCT_ID_SAMPLE, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(1, result.getBody().getContent().size()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getContent().iterator().next().getUsername()); + } + + @Test + void testFindFeedback() { + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + when(service.findFeedback(FEEDBACK_ID_SAMPLE)).thenReturn(mockFeedback); + when(userService.findUser(any())).thenReturn(mockUser); + var result = feedbackController.findFeedback(FEEDBACK_ID_SAMPLE); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getUsername()); + } + + @Test + void testFindFeedbackByUserIdAndProductId() { + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + when(service.findFeedbackByUserIdAndProductId(any(), any())).thenReturn(mockFeedback); + when(userService.findUser(any())).thenReturn(mockUser); + var result = feedbackController.findFeedbackByUserIdAndProductId(USER_ID_SAMPLE, PRODUCT_ID_SAMPLE); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getUsername()); + } + + @Test + void testCreateFeedback() { + Feedback mockFeedback = createFeedbackMock(); + Claims mockClaims = createMockClaims(); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + when(jwtService.validateToken(TOKEN_SAMPLE)).thenReturn(true); + when(jwtService.getClaimsFromToken(TOKEN_SAMPLE)).thenReturn(mockClaims); + when(service.upsertFeedback(any())).thenReturn(mockFeedback); + + var result = feedbackController.createFeedback(mockFeedback, "Bearer " + TOKEN_SAMPLE); + assertEquals(HttpStatus.CREATED, result.getStatusCode()); + assertTrue(result.getHeaders().getLocation().toString().contains(mockFeedback.getId())); + } + + private Feedback createFeedbackMock() { + Feedback mockFeedback = new Feedback(); + mockFeedback.setId(FEEDBACK_ID_SAMPLE); + mockFeedback.setUserId(USER_ID_SAMPLE); + mockFeedback.setProductId(PRODUCT_ID_SAMPLE); + mockFeedback.setContent("Great product!"); + mockFeedback.setRating(5); + return mockFeedback; + } + + private User createUserMock() { + User mockUser = new User(); + mockUser.setId(USER_ID_SAMPLE); + mockUser.setUsername("testUser"); + mockUser.setName("Test User"); + mockUser.setAvatarUrl("http://avatar.url"); + mockUser.setProvider("local"); + return mockUser; + } + + private Claims createMockClaims() { + Claims claims = new io.jsonwebtoken.impl.DefaultClaims(); + claims.setSubject(USER_ID_SAMPLE); + return claims; + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java new file mode 100644 index 000000000..eff891be0 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -0,0 +1,66 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.entity.User; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OAuth2ControllerTest { + + @Mock + private GitHubService gitHubService; + + @Mock + private JwtService jwtService; + + @InjectMocks + private OAuth2Controller oAuth2Controller; + + private Oauth2AuthorizationCode oauth2AuthorizationCode; + + @BeforeEach + void setup() { + oauth2AuthorizationCode = new Oauth2AuthorizationCode(); + oauth2AuthorizationCode.setCode("sampleCode"); + } + + @Test + void testGitHubLogin() { + String accessToken = "sampleAccessToken"; + User user = createUserMock(); + String jwtToken = "sampleJwtToken"; + + when(gitHubService.getAccessToken(any(), any(), any())).thenReturn(Map.of("access_token", accessToken)); + when(gitHubService.getAndUpdateUser(accessToken)).thenReturn(user); + when(jwtService.generateToken(user)).thenReturn(jwtToken); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(200, response.getStatusCodeValue()); + assertEquals(Map.of("token", jwtToken), response.getBody()); + } + + private User createUserMock() { + User user = new User(); + user.setId("userId"); + user.setUsername("username"); + user.setName("User Name"); + user.setAvatarUrl("http://avatar.url"); + user.setProvider("github"); + return user; + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 00417f662..0d984281e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -1,12 +1,13 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.util.List; - +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.model.MultilingualismValue; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,16 +24,16 @@ import org.springframework.hateoas.PagedModel.PageMetadata; import org.springframework.http.HttpStatus; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.service.ProductService; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductControllerTest { + private static final String PRODUCT_ID_SAMPLE = "amazon-comprehend"; private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; private static final String PRODUCT_NAME_DE_SAMPLE = "Amazon Comprehend DE"; private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; @@ -112,4 +113,12 @@ private Product createProductMock() { mockProduct.setTags(List.of("AI")); return mockProduct; } -} \ No newline at end of file + + private ProductRating createProductRatingMock() { + ProductRating productRatingMock = new ProductRating(); + productRatingMock.setStarRating(1); + productRatingMock.setPercent(10); + productRatingMock.setCommentNumber(5); + return productRatingMock; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 814044478..f188b3f4c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -28,6 +28,7 @@ @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { + public static final String TAG = "v10.0.6"; @Mock private ProductService productService; @@ -41,6 +42,7 @@ class ProductDetailsControllerTest { private ProductDetailsController productDetailsController; private static final String PRODUCT_NAME_SAMPLE = "Docker"; private static final String PRODUCT_NAME_DE_SAMPLE = "Docker DE"; + public static final String DOCKER_CONNECTOR_ID = "docker-connector"; @Test void testProductDetails() { @@ -49,15 +51,31 @@ void testProductDetails() { ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); - ResponseEntity result = productDetailsController.findProductDetails("docker-connector"); + ResponseEntity result = productDetailsController.findProductDetails(DOCKER_CONNECTOR_ID); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(result, mockExpectedResult); - verify(productService, times(1)).fetchProductDetail("docker-connector"); + verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); verify(detailModelAssembler, times(1)).toModel(mockProduct(), null); } + @Test + void testProductDetailsWithVersion() { + Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG)).thenReturn(createProductMockWithDetails()); + ResponseEntity mockExpectedResult = + new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); + + ResponseEntity result = + productDetailsController.findProductDetailsByVersion(DOCKER_CONNECTOR_ID, TAG); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(result, mockExpectedResult); + + verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); + } + @Test void testFindProductVersionsById() { List models = List.of(new MavenArtifactVersionModel()); @@ -73,7 +91,7 @@ void testFindProductVersionsById() { private Product mockProduct() { Product mockProduct = new Product(); - mockProduct.setId("docker-connector"); + mockProduct.setId(DOCKER_CONNECTOR_ID); MultilingualismValue name = new MultilingualismValue(); name.setEn(PRODUCT_NAME_SAMPLE); name.setDe(PRODUCT_NAME_DE_SAMPLE); @@ -84,7 +102,7 @@ private Product mockProduct() { private ProductDetailModel createProductMockWithDetails() { ProductDetailModel mockProductDetail = new ProductDetailModel(); - mockProductDetail.setId("docker-connector"); + mockProductDetail.setId(DOCKER_CONNECTOR_ID); MultilingualismValue name = new MultilingualismValue(); name.setEn(PRODUCT_NAME_SAMPLE); name.setDe(PRODUCT_NAME_DE_SAMPLE); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java deleted file mode 100644 index 5886b6473..000000000 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.axonivy.market.controller; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.axonivy.market.service.UserService; - -@ExtendWith(MockitoExtension.class) -class UserControllerTest { - - @Mock - UserService userService; - - @InjectMocks - UserController userController; - - @Test - void testGetAllUser() { - var result = userController.getAllUser(); - assertNotEquals(null, result); - } -} diff --git a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java index 3bbe6f80b..ad5db1617 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java @@ -1,9 +1,10 @@ package com.axonivy.market.handler; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +import com.axonivy.market.exceptions.ExceptionHandlers; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.Message; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,11 +12,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import com.axonivy.market.exceptions.ExceptionHandlers; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.Message; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ExceptionHandlersTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java new file mode 100644 index 000000000..51a510357 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java @@ -0,0 +1,189 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.repository.FeedbackRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.FeedbackServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FeedbackServiceImplTest { + + @Mock + private FeedbackRepository feedbackRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProductRepository productRepository; + + @InjectMocks + private FeedbackServiceImpl feedbackService; + + @BeforeEach + void setUp() { + // Mock initialization or setup if needed + } + + @Test + void testFindFeedbacks_ProductNotFound() { + String productId = "nonExistingProduct"; + + when(productRepository.findById(productId)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbacks(productId, Pageable.unpaged())); + + verify(productRepository, times(1)).findById(productId); + verify(feedbackRepository, never()).searchByProductId(any(), any()); + } + + @Test + void testFindFeedback_Success() throws NotFoundException { + // Mock data + String feedbackId = "feedback123"; + Feedback mockFeedback = new Feedback(); + mockFeedback.setId(feedbackId); + + // Mock behavior + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(mockFeedback)); + + // Test method + Feedback result = feedbackService.findFeedback(feedbackId); + + // Verify + assertEquals(mockFeedback, result); + verify(feedbackRepository, times(1)).findById(feedbackId); + } + + @Test + void testFindFeedback_NotFound() { + // Mock data + String nonExistingId = "nonExistingFeedbackId"; + + // Mock behavior + when(feedbackRepository.findById(nonExistingId)).thenReturn(Optional.empty()); + + // Test and verify exception + assertThrows(NotFoundException.class, () -> feedbackService.findFeedback(nonExistingId)); + + // Verify interactions + verify(feedbackRepository, times(1)).findById(nonExistingId); + } + + @Test + void testFindFeedbackByUserIdAndProductId_UserNotFound() { + // Mock data + String nonExistingUserId = "nonExistingUser"; + String productId = "product123"; + + // Mock behavior + when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); + + // Test and verify exception + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbackByUserIdAndProductId(nonExistingUserId, productId)); + + // Verify interactions + verify(userRepository, times(1)).findById(nonExistingUserId); + verify(feedbackRepository, never()).findByUserIdAndProductId(any(), any()); + } + + @Test + void testUpsertFeedback_NewFeedback() throws NotFoundException { + // Mock data + Feedback newFeedback = new Feedback(); + newFeedback.setUserId("user123"); + newFeedback.setProductId("product123"); + newFeedback.setContent("Great product!"); + newFeedback.setRating(5); + + User u = new User(); + u.setId(newFeedback.getUserId()); + when(userRepository.findById(newFeedback.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId())).thenReturn(null); + when(feedbackRepository.save(newFeedback)).thenReturn(newFeedback); + + // Test method + Feedback result = feedbackService.upsertFeedback(newFeedback); + + // Verify + assertEquals(newFeedback, result); + verify(userRepository, times(1)).findById(newFeedback.getUserId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId()); + verify(feedbackRepository, times(1)).save(newFeedback); + } + + @Test + void testUpsertFeedback_UpdateFeedback() throws NotFoundException { + // Mock data + Feedback existingFeedback = new Feedback(); + existingFeedback.setId("existingFeedback123"); + existingFeedback.setUserId("user123"); + existingFeedback.setProductId("product123"); + existingFeedback.setContent("Good product!"); + existingFeedback.setRating(4); + + User u = new User(); + u.setId(existingFeedback.getUserId()); + when(userRepository.findById(existingFeedback.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId())).thenReturn(existingFeedback); + when(feedbackRepository.save(existingFeedback)).thenReturn(existingFeedback); + + // Test method + Feedback updatedFeedback = new Feedback(); + updatedFeedback.setId(existingFeedback.getId()); + updatedFeedback.setUserId(existingFeedback.getUserId()); + updatedFeedback.setProductId(existingFeedback.getProductId()); + updatedFeedback.setContent("Excellent product!"); + updatedFeedback.setRating(5); + + Feedback result = feedbackService.upsertFeedback(updatedFeedback); + + // Verify + assertEquals(updatedFeedback.getId(), result.getId()); + assertEquals(updatedFeedback.getContent(), result.getContent()); + assertEquals(updatedFeedback.getRating(), result.getRating()); + verify(userRepository, times(1)).findById(existingFeedback.getUserId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId()); + verify(feedbackRepository, times(1)).save(existingFeedback); + } + + @Test + void testGetProductRatingById_NoFeedbacks() { + // Mock data + String productId = "product123"; + + // Mock behavior + when(feedbackRepository.findByProductId(productId)).thenReturn(new ArrayList<>()); + + // Test method + List result = feedbackService.getProductRatingById(productId); + + // Verify + assertEquals(5, result.size()); // Expect ratings for stars 1 to 5 + result.forEach(rating -> { + assertEquals(0, rating.getCommentNumber()); + assertEquals(0, rating.getPercent()); + }); + verify(feedbackRepository, times(1)).findByProductId(productId); + } +} + diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java index fa74bc54a..0e1f06f4b 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -1,32 +1,27 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHCommit.File; -import org.kohsuke.github.GHCompare; +import org.kohsuke.github.*; import org.kohsuke.github.GHCompare.Commit; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.PagedIterable; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GHAxonIvyMarketRepoServiceImplTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index e26226c6b..bbd8416fa 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -1,13 +1,8 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; - import com.axonivy.market.github.service.impl.GitHubServiceImpl; +import com.axonivy.market.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; @@ -15,7 +10,16 @@ import org.kohsuke.github.GitHub; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class GitHubServiceImplTest { @@ -27,9 +31,25 @@ class GitHubServiceImplTest { @Mock GHRepository ghRepository; + @Mock + private RestTemplateBuilder restTemplateBuilder; + + @Mock + private RestTemplate restTemplate; + + @Mock + private UserRepository userRepository; + @InjectMocks private GitHubServiceImpl gitHubService; + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + // Use lenient stubbing + lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate); + } + @Test void testGetGithub() throws IOException { var result = gitHubService.getGitHub(); @@ -51,5 +71,4 @@ void testGetDirectoryContent() throws IOException { var result = gitHubService.getDirectoryContent(ghRepository, "", ""); assertEquals(0, result.size()); } - } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java new file mode 100644 index 000000000..4eb8ffcec --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java @@ -0,0 +1,94 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.User; +import com.axonivy.market.service.impl.JwtServiceImpl; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class JwtServiceImplTest { + + private static final String SECRET = "mySecret"; + private static final long EXPIRATION = 7L; // 7 days + + @InjectMocks + private JwtServiceImpl jwtService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(jwtService, "secret", SECRET); + ReflectionTestUtils.setField(jwtService, "expiration", EXPIRATION); + } + + @Test + void testGenerateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + assertNotNull(token); + assertFalse(token.isEmpty()); + + Claims claims = jwtService.getClaimsFromToken(token); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testValidateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String validToken = jwtService.generateToken(user); + assertTrue(jwtService.validateToken(validToken)); + + String invalidToken = "invalid.token.here"; + assertFalse(jwtService.validateToken(invalidToken)); + } + + @Test + void testGetClaimsFromToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Claims claims = jwtService.getClaimsFromToken(token); + assertNotNull(claims); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testGetClaimsJws() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Jws claimsJws = jwtService.getClaimsJws(token); + assertNotNull(claimsJws); + assertNotNull(claimsJws.getBody()); + assertEquals("123", claimsJws.getBody().getSubject()); + } +} + diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index f23d6ea21..8562d8f66 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -1,15 +1,14 @@ package com.axonivy.market.service; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.verify; - +import com.axonivy.market.schedulingtask.ScheduledTasks; import org.awaitility.Awaitility; import org.awaitility.Durations; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; -import com.axonivy.market.schedulingtask.ScheduledTasks; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; @SpringBootTest class SchedulingTasksTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java index d14aaff8c..dd786a093 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java @@ -1,7 +1,8 @@ package com.axonivy.market.service; -import java.util.List; - +import com.axonivy.market.entity.User; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.UserServiceImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,9 +11,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.entity.User; -import com.axonivy.market.repository.UserRepository; -import com.axonivy.market.service.impl.UserServiceImpl; +import java.util.List; @ExtendWith(MockitoExtension.class) class UserServiceImplTest { @@ -28,8 +27,7 @@ void testFindAllUser() { // Mock data and service User mockUser = new User(); mockUser.setId("123"); - mockUser.setUsername("tvtTest"); - mockUser.setPassword("12345"); + mockUser.setName("tvtTest"); List mockResultReturn = List.of(mockUser); Mockito.when(userRepository.findAll()).thenReturn(mockResultReturn); @@ -39,4 +37,19 @@ void testFindAllUser() { // Verify Assertions.assertEquals(result, mockResultReturn); } + + @Test + void testCreateUser() { + // Mock data + User mockUser = new User(); + mockUser.setId("123"); + mockUser.setName("tvtTest"); + Mockito.when(userRepository.save(mockUser)).thenReturn(mockUser); + + // Exercise + User result = employeeService.createUser(mockUser); + + // Verify + Assertions.assertEquals(result, mockUser); + } } From 40e9d293c354516cd6fbcd27d1734016a8bef412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= <143604440+tvtphuc-axonivy@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:04:27 +0700 Subject: [PATCH 22/62] Feature/marp 473 follow up for marp 264 installation frequency providing data (#35) --- .github/workflows/service-dev-build.yml | 2 +- .../ProductDetailModelAssembler.java | 1 + .../controller/ProductDetailsController.java | 24 ++++-- .../com/axonivy/market/entity/Product.java | 12 ++- .../market/model/ProductDetailModel.java | 1 + .../market/service/ProductService.java | 1 + .../service/impl/ProductServiceImpl.java | 73 ++++++++++++++-- .../src/main/resources/application.properties | 1 + .../ProductDetailsControllerTest.java | 23 +++-- .../service/ProductServiceImplTest.java | 86 ++++++++++++++++--- .../market/service/SchedulingTasksTest.java | 2 +- 11 files changed, 184 insertions(+), 42 deletions(-) diff --git a/.github/workflows/service-dev-build.yml b/.github/workflows/service-dev-build.yml index e310a9378..3700e5322 100644 --- a/.github/workflows/service-dev-build.yml +++ b/.github/workflows/service-dev-build.yml @@ -39,4 +39,4 @@ jobs: - name: Restart Tomcat server run: | sudo systemctl stop tomcat - sudo systemctl start tomcat + sudo systemctl start tomcat \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index dbfa0be0b..ff9ade568 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -58,6 +58,7 @@ private void createDetailResource(ProductDetailModel model, Product product, Str model.setCompatibility(product.getCompatibility()); model.setContactUs(product.getContactUs()); model.setCost(product.getCost()); + model.setInstallationCount(product.getInstallationCount()); if (StringUtils.isBlank(tag) && StringUtils.isNotBlank(product.getNewestReleaseVersion())) { tag = product.getNewestReleaseVersion(); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index e83712718..87fa6b218 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -1,22 +1,25 @@ package com.axonivy.market.controller; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.service.VersionService; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; + +import java.util.List; + import org.springframework.http.HttpStatus; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; + import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; -import org.springframework.web.bind.annotation.PathVariable; - -import java.util.List; - -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; +import io.swagger.v3.oas.annotations.Operation; @RestController @RequestMapping(PRODUCT_DETAILS) @@ -39,6 +42,13 @@ public ResponseEntity findProductDetailsByVersion(@PathVaria return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); } + @Operation(summary = "increase installation count by 1", description = "increase installation count by 1") + @PutMapping("/installationcount/{key}") + public ResponseEntity syncInstallationCount(@PathVariable("key") String key) { + int result = productService.updateInstallationCountForProduct(key); + return new ResponseEntity<>(result, HttpStatus.OK); + } + @GetMapping("/{id}") public ResponseEntity findProductDetails(@PathVariable("id") String id) { var productDetail = productService.fetchProductDetail(id); diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index deb469a61..b1a135c31 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -6,15 +6,11 @@ import java.util.Date; import java.util.List; + import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - +import lombok.*; import com.axonivy.market.github.model.MavenArtifact; - import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.data.annotation.Id; @@ -24,6 +20,7 @@ @Setter @AllArgsConstructor @NoArgsConstructor +@Builder @Document(PRODUCT) public class Product implements Serializable { private static final long serialVersionUID = -8770801877877277258L; @@ -51,11 +48,12 @@ public class Product implements Serializable { private String compatibility; private Boolean validate; private Boolean contactUs; - private Integer installationCount; + private int installationCount; private Date newestPublishedDate; private String newestReleaseVersion; private List productModuleContents; private List artifacts; + private Boolean synchronizedInstallationCount; @Override public int hashCode() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java index 2943ccc79..99a13922f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -22,6 +22,7 @@ public class ProductDetailModel extends ProductModel { private String compatibility; private Boolean contactUs; private ProductModuleContent productModuleContent; + private int installationCount; @Override public int hashCode() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index a44f60668..89bef2d95 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -9,6 +9,7 @@ public interface ProductService { boolean syncLatestDataFromMarketRepo(); + int updateInstallationCountForProduct(String key); Product fetchProductDetail(String id); String getCompatibilityFromOldestTag(String oldestTag); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index fd22c680c..04de791d5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -5,18 +5,25 @@ import java.io.IOException; import java.net.URL; -import java.util.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.github.util.GitHubUtils; -import com.axonivy.market.entity.ProductModuleContent; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -26,19 +33,24 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.enums.FileType; import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; import com.axonivy.market.factory.ProductFactory; import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; -import com.axonivy.market.entity.GitHubRepoMeta; -import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ProductService; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; @@ -54,9 +66,13 @@ public class ProductServiceImpl implements ProductService { private GHCommit lastGHCommit; private GitHubRepoMeta marketRepoMeta; + private final ObjectMapper mapper = new ObjectMapper(); - public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + @Value("${synchronized.installation.counts.path}") + private String installationCountPath; + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + private final Random random = new Random(); public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService) { @@ -108,6 +124,37 @@ public boolean syncLatestDataFromMarketRepo() { return isAlreadyUpToDate; } + @Override + public int updateInstallationCountForProduct(String key) { + return productRepository.findById(key).map(product -> { + log.info("updating installation count for product {}", key); + if (!BooleanUtils.isTrue(product.getSynchronizedInstallationCount())) { + syncInstallationCountWithProduct(product); + } + product.setInstallationCount(product.getInstallationCount() + 1); + return productRepository.save(product); + }).map(Product::getInstallationCount).orElse(0); + } + + private void syncInstallationCountWithProduct(Product product) { + log.info("synchronizing installation count for product {}", product.getId()); + try { + String installationCounts = Files.readString(Paths.get(installationCountPath)); + Map mapping = mapper.readValue(installationCounts, + new TypeReference>(){}); + List keyList = mapping.keySet().stream().toList(); + int currentInstallationCount = keyList.contains(product.getId()) + ? mapping.get(product.getId()) + : random.nextInt(20, 50); + product.setInstallationCount(currentInstallationCount); + product.setSynchronizedInstallationCount(true); + log.info("synchronized installation count for product {} successfully", product.getId()); + } catch (IOException ex) { + log.error(ex.getMessage()); + log.error("Could not read the marketplace-installation file to synchronize"); + } + } + private void syncRepoMetaDataStatus() { if (lastGHCommit == null) { return; @@ -273,6 +320,7 @@ private void updateProductFromReleaseTags(Product product) { } // Cover 3 cases after removing non-numeric characters (8, 11.1 and 10.0.2) + @Override public String getCompatibilityFromOldestTag(String oldestTag) { if (!oldestTag.contains(CommonConstants.DOT_SEPARATOR)) { return oldestTag + ".0+"; @@ -287,6 +335,13 @@ public String getCompatibilityFromOldestTag(String oldestTag) { @Override public Product fetchProductDetail(String id) { - return productRepository.findById(id).orElse(null); + Product product = productRepository.findById(id).orElse(null); + return Optional.ofNullable(product).map(productItem -> { + if (!BooleanUtils.isTrue(productItem.getSynchronizedInstallationCount())) { + syncInstallationCountWithProduct(productItem); + return productRepository.save(productItem); + } + return productItem; + }).orElse(null); } } diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 878e93e0f..aebb71018 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,6 +9,7 @@ springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] market.cors.allowed.origin.maxAge=3600 +synchronized.installation.counts.path=${MARKETPLACE_INSTALLATION_URL} spring.security.oauth2.client.registration.github.client-id= spring.security.oauth2.client.registration.github.client-secret= jwt.secret= diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index f188b3f4c..0ac125d07 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -3,28 +3,28 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Objects; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.service.VersionService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; - import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import java.util.List; -import java.util.Objects; - import com.axonivy.market.assembler.ProductDetailModelAssembler; import com.axonivy.market.entity.Product; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { @@ -89,6 +89,15 @@ void testFindProductVersionsById() { Assertions.assertEquals(models, result.getBody()); } + @Test + void testSyncInstallationCount() { + when(productService.updateInstallationCountForProduct("google-maps-connector")).thenReturn(1); + + var result = productDetailsController.syncInstallationCount("google-maps-connector"); + + assertEquals(1, result.getBody()); + } + private Product mockProduct() { Product mockProduct = new Product(); mockProduct.setId(DOCKER_CONNECTOR_ID); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 9fd472991..841cfc7f5 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -1,49 +1,68 @@ package com.axonivy.market.service; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.MetaConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; -import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.model.MultilingualismValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.*; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.kohsuke.github.PagedIterable; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; import com.axonivy.market.enums.SortOption; import com.axonivy.market.enums.TypeOption; import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.ProductServiceImpl; @@ -77,11 +96,13 @@ class ProductServiceImplTest { @Mock private GitHubService gitHubService; + @Captor + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Product.class); @Mock private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; @Captor - ArgumentCaptor> argumentCaptor; + ArgumentCaptor> productListArgumentCaptor; @InjectMocks private ProductServiceImpl productService; @@ -91,6 +112,50 @@ public void setup() { mockResultReturn = createPageProductsMock(); } + @Test + void testUpdateInstallationCount() { + // prepare + Mockito.when(productRepository.findById("google-maps-connector")).thenReturn(Optional.of(mockProduct())); + + // exercise + productService.updateInstallationCountForProduct("google-maps-connector"); + + // Verify + verify(productRepository).save(argumentCaptor.capture()); + int updatedInstallationCount = argumentCaptor.getValue().getInstallationCount(); + + assertEquals(1, updatedInstallationCount); + verify(productRepository, times(1)).findById(Mockito.anyString()); + verify(productRepository, times(1)).save(Mockito.any()); + } + + @Test + void testSyncInstallationCountWithProduct() throws Exception { + // Mock data + ReflectionTestUtils.setField(productService, "installationCountPath", "path/to/installationCount.json"); + Product product = mockProduct(); + product.setSynchronizedInstallationCount(false); + Mockito.when(productRepository.findById("google-maps-connector")).thenReturn(Optional.of(product)); + Mockito.when(productRepository.save(any())).thenReturn(product); + // Mock the behavior of Files.readString and ObjectMapper.readValue + String installationCounts = "{\"google-maps-connector\": 10}"; + try (MockedStatic filesMockedStatic = mockStatic(Files.class)) { + when(Files.readString(Paths.get("path/to/installationCount.json"))).thenReturn(installationCounts); + // Call the method + int result = productService.updateInstallationCountForProduct("google-maps-connector"); + + // Verify the results + assertEquals(11, result); + assertEquals(true, product.getSynchronizedInstallationCount()); + assertTrue(product.getSynchronizedInstallationCount()); + } + } + + private Product mockProduct() { + return Product.builder().id("google-maps-connector").language("English").synchronizedInstallationCount(true) + .build(); + } + @Test void testFindProducts() { langague = "en"; @@ -240,9 +305,9 @@ void testSyncProductsFirstTime() throws IOException { // Executes productService.syncLatestDataFromMarketRepo(); - verify(productRepository).saveAll(argumentCaptor.capture()); + verify(productRepository).saveAll(productListArgumentCaptor.capture()); - assertThat(argumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() + assertThat(productListArgumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() .isEqualTo(List.of(mockReadmeProductContent())); } @@ -277,6 +342,7 @@ void testSearchProducts() { void testFetchProductDetail() { String id = "amazon-comprehend"; Product mockProduct = mockResultReturn.getContent().get(0); + mockProduct.setSynchronizedInstallationCount(true); when(productRepository.findById(id)).thenReturn(Optional.ofNullable(mockProduct)); Product result = productService.fetchProductDetail(id); assertEquals(mockProduct, result); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index 8562d8f66..70e4d0771 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -10,7 +10,7 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; -@SpringBootTest +@SpringBootTest(properties = { "marketPlace-installation-url=D:/marketplace-installation.json" }) class SchedulingTasksTest { @SpyBean From 2fef9db75eed6e42a24c0cde50f50000f3cf8a24 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Thu, 18 Jul 2024 17:16:44 +0700 Subject: [PATCH 23/62] Integrated marketplace-ui(develop) inside marketplace(develop) --- marketplace-ui/.github/workflows/ci-build.yml | 57 + .../.github/workflows/dev-build.yml | 28 + marketplace-ui/LICENSE | 201 + marketplace-ui/SECURITY.md | 25 + marketplace-ui/angular.json | 26 +- marketplace-ui/package-lock.json | 6258 +++++++++++------ marketplace-ui/package.json | 7 + marketplace-ui/sonar-project.properties | 2 +- marketplace-ui/src/app/app.config.ts | 25 +- marketplace-ui/src/app/app.routes.ts | 10 +- .../src/app/auth/auth.service.spec.ts | 133 + marketplace-ui/src/app/auth/auth.service.ts | 140 + .../github-callback.component.spec.ts | 56 + .../github-callback.component.ts | 24 + .../src/app/core/configs/markdown.config.ts | 16 + .../src/app/core/configs/translate.config.ts | 2 +- .../app/core/interceptors/api.interceptor.ts | 18 +- .../language/language.service.spec.ts | 6 +- .../services/language/language.service.ts | 22 +- .../core/services/loading/loading.service.ts | 2 +- .../app/core/services/theme/theme.service.ts | 2 +- .../app/modules/home/home.component.spec.ts | 2 +- .../product-card/product-card.component.html | 6 +- .../product-card.component.spec.ts | 5 +- .../product-card/product-card.component.ts | 2 +- .../product-detail-feedback.component.html | 15 + .../product-detail-feedback.component.scss | 8 + .../product-detail-feedback.component.spec.ts | 134 + .../product-detail-feedback.component.ts | 87 + .../feedback-filter.component.html | 14 + .../feedback-filter.component.scss | 20 + .../feedback-filter.component.spec.ts | 65 + .../feedback-filter.component.ts | 25 + .../product-feedback.service.spec.ts | 133 + .../product-feedback.service.ts | 145 + .../product-feedback.component.html | 34 + .../product-feedback.component.scss | 89 + .../product-feedback.component.spec.ts | 53 + .../product-feedback.component.ts | 36 + .../product-feedbacks-panel.component.html | 27 + .../product-feedbacks-panel.component.scss | 70 + .../product-feedbacks-panel.component.spec.ts | 72 + .../product-feedbacks-panel.component.ts | 66 + .../add-feedback-dialog.component.html | 51 + .../add-feedback-dialog.component.scss | 95 + .../add-feedback-dialog.component.spec.ts | 125 + .../add-feedback-dialog.component.ts | 68 + .../success-dialog.component.html | 13 + .../success-dialog.component.scss | 9 + .../success-dialog.component.spec.ts | 53 + .../success-dialog.component.ts | 19 + .../product-star-rating-panel.component.html | 29 + .../product-star-rating-panel.component.scss | 66 + ...roduct-star-rating-panel.component.spec.ts | 103 + .../product-star-rating-panel.component.ts | 33 + .../product-star-rating.service.spec.ts | 81 + .../product-star-rating.service.ts | 66 + .../star-rating-highlight.directive.spec.ts | 52 + .../star-rating-highlight.directive.ts | 20 + .../show-feedbacks-dialog.component.html | 20 + .../show-feedbacks-dialog.component.scss | 48 + .../show-feedbacks-dialog.component.spec.ts | 77 + .../show-feedbacks-dialog.component.ts | 25 + ...duct-detail-information-tab.component.html | 101 + ...duct-detail-information-tab.component.scss | 21 + ...t-detail-information-tab.component.spec.ts | 30 + ...roduct-detail-information-tab.component.ts | 18 + ...roduct-detail-maven-content.component.html | 20 + ...roduct-detail-maven-content.component.scss | 0 ...uct-detail-maven-content.component.spec.ts | 26 + .../product-detail-maven-content.component.ts | 17 + ...oduct-detail-version-action.component.html | 171 +- ...oduct-detail-version-action.component.scss | 203 +- ...ct-detail-version-action.component.spec.ts | 92 +- ...product-detail-version-action.component.ts | 57 +- .../product-detail.component.html | 243 +- .../product-detail.component.scss | 262 + .../product-detail.component.spec.ts | 233 +- .../product-detail.component.ts | 227 +- .../product-detail.service.spec.ts | 38 + .../product-detail/product-detail.service.ts | 10 + ...t-installation-count-action.component.html | 9 + ...t-installation-count-action.component.scss | 29 + ...nstallation-count-action.component.spec.ts | 29 + ...uct-installation-count-action.component.ts | 17 + .../product-star-rating-number.component.html | 46 + .../product-star-rating-number.component.scss | 40 + ...oduct-star-rating-number.component.spec.ts | 78 + .../product-star-rating-number.component.ts | 36 + .../product-filter.component.html | 30 +- .../product-filter.component.ts | 7 +- .../modules/product/product.component.html | 2 +- .../modules/product/product.component.spec.ts | 51 +- .../app/modules/product/product.component.ts | 52 +- .../src/app/modules/product/product.routes.ts | 6 +- .../modules/product/product.service.spec.ts | 78 +- .../app/modules/product/product.service.ts | 40 +- .../components/footer/footer.component.html | 36 +- .../components/footer/footer.component.ts | 6 +- .../components/header/header.component.html | 2 +- .../components/header/header.component.ts | 11 +- .../language-selection.component.html | 12 +- .../language-selection.component.spec.ts | 5 +- .../language-selection.component.ts | 8 +- .../navigation/navigation.component.spec.ts | 1 + .../header/navigation/navigation.component.ts | 2 +- .../search-bar/search-bar.component.html | 106 +- .../header/search-bar/search-bar.component.ts | 15 +- .../star-rating/star-rating.component.html | 13 + .../star-rating/star-rating.component.scss | 45 + .../star-rating/star-rating.component.spec.ts | 51 + .../star-rating/star-rating.component.ts | 22 + .../app/shared/constants/common.constant.ts | 52 + .../app/shared/enums/feedback-sort-type.ts | 6 + .../src/app/shared/mocks/mock-data.ts | 206 +- .../src/app/shared/mocks/mock-services.ts | 18 +- .../models/apis/feedback-response.model.ts | 11 + .../models/apis/product-response.model.ts | 8 +- .../src/app/shared/models/criteria.model.ts | 7 +- .../app/shared/models/display-value.model.ts | 4 +- .../src/app/shared/models/feedback.model.ts | 10 + .../app/shared/models/product-detail.model.ts | 29 + .../models/product-module-content.model.ts | 13 + .../models/star-rating-counting.model.ts | 5 + .../src/app/shared/pipes/icon.pipe.ts | 20 + .../shared/services/app-modal.service.spec.ts | 57 + .../app/shared/services/app-modal.service.ts | 40 + marketplace-ui/src/assets/i18n/de.yaml | 50 +- marketplace-ui/src/assets/i18n/en.yaml | 50 +- .../src/assets/images/misc/avatar-default.png | Bin 0 -> 14350 bytes .../src/assets/images/misc/confetti-icon.png | Bin 0 -> 40988 bytes .../src/assets/images/misc/message-star.svg | 13 + .../src/assets/scss/custom-modal-style.scss | 44 + .../src/assets/scss/custom-style.scss | 81 +- .../environments/environment.development.ts | 5 +- .../src/environments/environment.ts | 5 +- marketplace-ui/src/styles.scss | 10 +- 137 files changed, 9943 insertions(+), 2676 deletions(-) create mode 100644 marketplace-ui/.github/workflows/ci-build.yml create mode 100644 marketplace-ui/.github/workflows/dev-build.yml create mode 100644 marketplace-ui/LICENSE create mode 100644 marketplace-ui/SECURITY.md create mode 100644 marketplace-ui/src/app/auth/auth.service.spec.ts create mode 100644 marketplace-ui/src/app/auth/auth.service.ts create mode 100644 marketplace-ui/src/app/auth/github-callback/github-callback.component.spec.ts create mode 100644 marketplace-ui/src/app/auth/github-callback/github-callback.component.ts create mode 100644 marketplace-ui/src/app/core/configs/markdown.config.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts create mode 100644 marketplace-ui/src/app/shared/components/star-rating/star-rating.component.html create mode 100644 marketplace-ui/src/app/shared/components/star-rating/star-rating.component.scss create mode 100644 marketplace-ui/src/app/shared/components/star-rating/star-rating.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/star-rating/star-rating.component.ts create mode 100644 marketplace-ui/src/app/shared/enums/feedback-sort-type.ts create mode 100644 marketplace-ui/src/app/shared/models/apis/feedback-response.model.ts create mode 100644 marketplace-ui/src/app/shared/models/feedback.model.ts create mode 100644 marketplace-ui/src/app/shared/models/product-detail.model.ts create mode 100644 marketplace-ui/src/app/shared/models/product-module-content.model.ts create mode 100644 marketplace-ui/src/app/shared/models/star-rating-counting.model.ts create mode 100644 marketplace-ui/src/app/shared/pipes/icon.pipe.ts create mode 100644 marketplace-ui/src/app/shared/services/app-modal.service.spec.ts create mode 100644 marketplace-ui/src/app/shared/services/app-modal.service.ts create mode 100644 marketplace-ui/src/assets/images/misc/avatar-default.png create mode 100644 marketplace-ui/src/assets/images/misc/confetti-icon.png create mode 100644 marketplace-ui/src/assets/images/misc/message-star.svg create mode 100644 marketplace-ui/src/assets/scss/custom-modal-style.scss diff --git a/marketplace-ui/.github/workflows/ci-build.yml b/marketplace-ui/.github/workflows/ci-build.yml new file mode 100644 index 000000000..8a437d1f4 --- /dev/null +++ b/marketplace-ui/.github/workflows/ci-build.yml @@ -0,0 +1,57 @@ +name: CI Build +run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} + +on: + push: + branches-ignore: + - develop + - master + workflow_dispatch: + +jobs: + build: + name: Build + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - name: Install Dependencies + run: npm install + - name: Build project + run: npm run build + + analysis: + name: Sonarqube + needs: build + runs-on: self-hosted + env: + SONAR_PROJECT_KEY: 'AxonIvy-Market-UI' + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + steps: + - name: Execute Tests + run: npm run test + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + with: + args: + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + - name: SonarQube Quality Gate check + id: sonarqube-quality-gate-check + uses: sonarsource/sonarqube-quality-gate-action@master + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + + - name: Clean up + run: | + rm -rf * diff --git a/marketplace-ui/.github/workflows/dev-build.yml b/marketplace-ui/.github/workflows/dev-build.yml new file mode 100644 index 000000000..cb34d8665 --- /dev/null +++ b/marketplace-ui/.github/workflows/dev-build.yml @@ -0,0 +1,28 @@ +name: Dev Build +run-name: Build and Deploy Marketplace-UI on branch ${{github.ref_name}} by ${{github.actor}} + +on: + push: + branches: [ "develop" ] + workflow_dispatch: + +jobs: + build: + name: Build and deploy new code to Deployment directory + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + - name: Install Dependencies + run: npm install + - name: Build Angular app + run: npm run build -- --configuration production --output-path=dist + - name: Execute Tests + run: npm run test + - name: Copy files to Deployment directory + if: success() + run: sudo cp -r dist/* /var/www/marketplace-ui diff --git a/marketplace-ui/LICENSE b/marketplace-ui/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/marketplace-ui/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/marketplace-ui/SECURITY.md b/marketplace-ui/SECURITY.md new file mode 100644 index 000000000..1d4c06f71 --- /dev/null +++ b/marketplace-ui/SECURITY.md @@ -0,0 +1,25 @@ +## Reporting a Vulnerability + +At Axon Ivy, we take security seriously. If you believe you've found a security vulnerability in our software, we encourage you to let us know right away. We investigate all reported vulnerabilities promptly. + +To report a vulnerability, please send an email to [security@axonivy.com](mailto:security@axonivy.com) with the following information: + +- Description of the vulnerability +- Steps to reproduce the vulnerability +- Any additional information or context that may be helpful + +Please refrain from publicly disclosing the vulnerability until it has been addressed by our team. + +## Response Time + +We strive to respond to security vulnerability reports as quickly as possible. Upon receiving your report, we will acknowledge it within 72 hours and we will release a patch as soon as possible depending on complexity, but historically within a few days. +Please report (suspected) security vulnerabilities at https://support.axonivy.com/. + + +## Responsible Disclosure + +We encourage responsible disclosure of security vulnerabilities. We believe that working together with security researchers and the broader community helps us improve the security of our software for everyone. + +## Contact + +For any questions or concerns regarding security, please contact us at [security@axonivy.com](mailto:security@axonivy.com). diff --git a/marketplace-ui/angular.json b/marketplace-ui/angular.json index 7b92f41be..9ce46d2cc 100644 --- a/marketplace-ui/angular.json +++ b/marketplace-ui/angular.json @@ -20,14 +20,13 @@ "outputPath": "dist", "index": "src/index.html", "browser": "src/main.ts", - "polyfills": [], + "polyfills": [ + "zone.js", + "@angular/localize/init" + ], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", - "assets": [ - "src/favicon.ico", - "src/assets", - "src/assets/_market" - ], + "assets": ["src/favicon.ico", "src/assets", "src/assets/_market"], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss", @@ -97,18 +96,11 @@ "builder": "@angular-devkit/build-angular:karma", "options": { "codeCoverage": true, - "polyfills": [ - "zone.js", - "zone.js/testing" - ], + "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", - "assets": [ - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], + "assets": ["src/assets"], + "styles": ["src/styles.scss"], "scripts": [], "karmaConfig": "karma.conf.js", "sourceMap": true, @@ -121,4 +113,4 @@ "cli": { "analytics": false } -} \ No newline at end of file +} diff --git a/marketplace-ui/package-lock.json b/marketplace-ui/package-lock.json index dc4bce9eb..baefbf7d5 100644 --- a/marketplace-ui/package-lock.json +++ b/marketplace-ui/package-lock.json @@ -18,11 +18,18 @@ "@angular/platform-server": "^18.0.0", "@angular/router": "^18.0.0", "@fortawesome/fontawesome-free": "^6.5.2", + "@ng-bootstrap/ng-bootstrap": "^17.0.0", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", + "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "jwt-decode": "^4.0.0", "karma-viewport": "^1.0.9", + "marked": "^12.0.0", + "ngx-cookie-service": "^18.0.0", + "ngx-markdown": "^18.0.0", + "ngxtension": "^3.5.5", "rxjs": "~7.8.0", "tslib": "^2.3.0", "yaml": "^2.4.2", @@ -55,7 +62,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -65,12 +71,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1800.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1800.0.tgz", - "integrity": "sha512-B28h/+Og1F8/QWlizmOl3Iv3svH9uIJ456gw331RgtUMrYszU6WPlk1izG38PV++NKK9vv9NcqQsJCEvxY9ipg==", + "version": "0.1800.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1800.5.tgz", + "integrity": "sha512-KliFJTqwAIyRvW10JnJLlpXK86yx683unTgwgvkg9V4gUc/7cNCmWJiOCmYh1+gATpFq+3d3o36EdTzb4QS03g==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.0.0", + "@angular-devkit/core": "18.0.5", "rxjs": "7.8.1" }, "engines": { @@ -80,16 +86,16 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.0.0.tgz", - "integrity": "sha512-EZDn/2h24mldx8c8zbJ5BAz8YmXmPhdbFOILPixsTInJJ9/iKX+cFioyscqzRDkVuISMA8AagC+5E2ZIhCjiPQ==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.0.5.tgz", + "integrity": "sha512-itZN5tAZ+66bHZ4JNxIiPxfbSvQP6Gk4hcCzfGzcs3G0VsahR0rpX0Rg+1CRX1bpDzan3z8AVfwIxlLPKSOBbg==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1800.0", - "@angular-devkit/build-webpack": "0.1800.0", - "@angular-devkit/core": "18.0.0", - "@angular/build": "18.0.0", + "@angular-devkit/architect": "0.1800.5", + "@angular-devkit/build-webpack": "0.1800.5", + "@angular-devkit/core": "18.0.5", + "@angular/build": "18.0.5", "@babel/core": "7.24.5", "@babel/generator": "7.24.5", "@babel/helper-annotate-as-pure": "7.22.5", @@ -100,12 +106,11 @@ "@babel/preset-env": "7.24.5", "@babel/runtime": "7.24.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "18.0.0", + "@ngtools/webpack": "18.0.5", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.19", "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", @@ -115,6 +120,7 @@ "http-proxy-middleware": "3.0.0", "https-proxy-agent": "7.0.4", "inquirer": "9.2.22", + "istanbul-lib-instrument": "6.0.2", "jsonc-parser": "3.2.1", "karma-source-map-support": "1.4.0", "less": "4.2.0", @@ -209,70 +215,121 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "node_modules/@angular-devkit/build-angular/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, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", - "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" + "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": "opencollective", - "url": "https://opencollective.com/babel" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@angular-devkit/build-angular/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==", + "node_modules/@angular-devkit/build-angular/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, - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "node_modules/@angular-devkit/build-angular/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==", + "node_modules/@angular-devkit/build-angular/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 }, - "node_modules/@angular-devkit/build-angular/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "node_modules/@angular-devkit/build-angular/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 + }, + "node_modules/@angular-devkit/build-angular/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, + "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/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/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, + "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": { - "semver": "bin/semver.js" + "webpack": "bin/webpack.js" }, "engines": { - "node": ">=10" + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1800.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1800.0.tgz", - "integrity": "sha512-L61mW+aGK+opsokUZkj7q1/gnSyF3qz+FsAqdVyTvwBta3KKr8xzNR75fwvzZ9+qD8bum5oAOgtyw+tvPMMt3g==", + "version": "0.1800.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1800.5.tgz", + "integrity": "sha512-/eiIwlQJBZlCWLsfaoSOsSGFY24cLKCCY4fs/fvcBXxG5/g1FFx24Zt73j0qRoNeK3soUg9+lmCAiRvO6cGpJg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1800.0", + "@angular-devkit/architect": "0.1800.5", "rxjs": "7.8.1" }, "engines": { @@ -286,9 +343,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.0.0.tgz", - "integrity": "sha512-mFD4QgyM1SwPjk6slJsqAXX7oTNduYbA5zgyf29/9wNUagUaz0vdonwxFlHv+D5pPmX/tRY5mqxYD68F7FiC9g==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.0.5.tgz", + "integrity": "sha512-sGtrS0SqkcBvyuv0QkIfyadwPgDhMroz1r51lMh1hwzJaJ0LNuVMLviEeYIybeBnvAdp9YvYC8I1WgB/FUEFBw==", "dev": true, "dependencies": { "ajv": "8.13.0", @@ -312,30 +369,13 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@angular-devkit/schematics": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.0.0.tgz", - "integrity": "sha512-whvMDjnLd5ObyfO+HGZdPMtY8Ac+kVyVq2RigpKQmOoQOk8eMZw4iRsTOGzvaKXhFcFnTbT5O3c6Pvo42aCaAA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.0.5.tgz", + "integrity": "sha512-hZwAq3hwuJzCuh7uqO/7T9IMERhYVxz+ganJlEykpyr58o0IjUM1Q4ZSH5UOYlGRPdBCZJbfiafZ0Sg5w5xBww==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.0.0", + "@angular-devkit/core": "18.0.5", "jsonc-parser": "3.2.1", "magic-string": "0.30.10", "ora": "5.4.1", @@ -347,28 +387,33 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.1.0.tgz", + "integrity": "sha512-2JNlMEnCvLz8q1Qa4sWR9BddtpDWMKYguMzHJKm5zUDwH90CgWHolQlXumtpqbL8r78xd57t35IkbEFLF3UsQw==" + }, "node_modules/@angular/animations": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.0.0.tgz", - "integrity": "sha512-An/IqDBCyWZXVC23+jRKdmvJB/b4P1BVljZxGxF+CiocNd/xvVVeBYuuxzp3vhhVobyO8A9iD12itPudLOpt2Q==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.0.4.tgz", + "integrity": "sha512-xbdtBUvpTGEmVQkCoOad26LBMRy9ddM9pvCidMZBWXiM7NEuc3dfVT99a1cU4MZFiJeiQEvOWQn03iXskbBMGQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.0.0" + "@angular/core": "18.0.4" } }, "node_modules/@angular/build": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.0.0.tgz", - "integrity": "sha512-CVE/08mH7LhcHte0UN9ETZ+d7ewPPLbtdMXYnCNvbbAqfOCaPQ62agDzBE9sHOLlyn6fkFX2G4mwyKV+AQbQnw==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.0.5.tgz", + "integrity": "sha512-6C+azPDYqPWX9/+53OTyvzmAKxrGwgQcDnueC/Sc6NZJOAs2VsOIn5ULPtcRDlrf/Rbo0dGM4OvKCM2q1BRuBg==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1800.0", + "@angular-devkit/architect": "0.1800.5", "@babel/core": "7.24.5", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.24.5", @@ -429,73 +474,16 @@ } } }, - "node_modules/@angular/build/node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", - "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/build/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==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@angular/build/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 - }, - "node_modules/@angular/build/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/cli": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.0.0.tgz", - "integrity": "sha512-SzPMju4L7Lr59k72PNmEznCSfHGtoDSmDl3lbLoumnIKlZoejnIgEipzXSjTkBk23rHAAUevlpDUUhkOIoAppg==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.0.5.tgz", + "integrity": "sha512-w3NOdj6T7QhBmFleavc+AEhcAMyPkt7RsyWW2saufD6x55gzynGQZb9UBZwKDUAR6UtqchBX/HEBWCLNnjbiHg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1800.0", - "@angular-devkit/core": "18.0.0", - "@angular-devkit/schematics": "18.0.0", - "@schematics/angular": "18.0.0", + "@angular-devkit/architect": "0.1800.5", + "@angular-devkit/core": "18.0.5", + "@angular-devkit/schematics": "18.0.5", + "@schematics/angular": "18.0.5", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.2", @@ -519,45 +507,33 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/common": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.0.0.tgz", - "integrity": "sha512-s43ZcOhXTUlkdOPMiMtr4Pz1qKIS8nClXhaahY0JBQZYGsOSn7NR42SoEeB8/ixktfY60s3SLhizXTKMAYtOTA==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.0.4.tgz", + "integrity": "sha512-7WxZKLzSu5QtyLGrtlZrtUQlP3WfDR++yHr5jF9DJZ3IY35UutwiPCegCcq4Qh5X2xWqnRKGm20TLlKVoj0t5Q==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.0.0", + "@angular/core": "18.0.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.0.0.tgz", - "integrity": "sha512-KbyjUfpdVE8+6fiHqo4PgVrGppYUhlU1JVAj6dqeUug9lQ5HBcANfiZ7p8CA2lU3gvIZ1cj+ZDKA1NEB1wvvtQ==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.0.4.tgz", + "integrity": "sha512-OVPXtJo5SkGQUCioCVxKcRfEw48tz8xCtJGDXjVKWtyOkXnmWl8Y/e54mteiJd1KybXHvPLW0LPtWZYB06Qy7g==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.0.0" + "@angular/core": "18.0.4" }, "peerDependenciesMeta": { "@angular/core": { @@ -566,12 +542,11 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.0.0.tgz", - "integrity": "sha512-fy9MBSHDM/YAyrIWa15JV1ZrpuSc51HHUSA3W/UKrDqUqSfYyj11/0PeYkdIWUD/dACZSrEge3nVnYCjdyJqPA==", - "dev": true, + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.0.4.tgz", + "integrity": "sha512-pUv664JCZHKHsLDvO8iNjWXVHOB2ggKxVoxiowOMNpR4dqxrK/oOLGkPGltYUW/xF6Eajc7Zs0lK/R5uljoYQg==", "dependencies": { - "@babel/core": "7.24.4", + "@babel/core": "7.24.7", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", @@ -586,22 +561,78 @@ "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.0.0", + "@angular/compiler": "18.0.4", "typescript": ">=5.4 <5.5" } }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "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==" + }, + "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==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@angular/core": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.0.0.tgz", - "integrity": "sha512-tpR7HIY4MJuM9ETpG15IvBr1wsI8Cyec3ZxYFe/27FKHARvxDbqIrT9QevmC6lxg1NdfD990G2XphYML1EyJ8g==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.0.4.tgz", + "integrity": "sha512-k0AUZbJc0eyzRexvKlR1sR0qNhe54Om9ln6lRn7y1+gAsg+OwFDyF427fFuzqpZVe/MmpvX3CXWdl0twZAYEiA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", @@ -609,29 +640,28 @@ } }, "node_modules/@angular/forms": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.0.0.tgz", - "integrity": "sha512-Q+4WExdgALP7VJ5lKSYmpz8CtAFZI4f3n09JhExIZoPTLD/mqOJcxxO7wTc9lXG4jKSE8BlfgK2txKz1cQvrEQ==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.0.4.tgz", + "integrity": "sha512-LM2rVIuJa2fGxP0oCy0uFSGY6h9tyL64gtGp02QqKaVszG4oJ8wue0/VSbBtKyH0xEN4eOXDzOXbiahbtFhRZA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.0", - "@angular/core": "18.0.0", - "@angular/platform-browser": "18.0.0", + "@angular/common": "18.0.4", + "@angular/core": "18.0.4", + "@angular/platform-browser": "18.0.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.0.0.tgz", - "integrity": "sha512-DW3wB5Cj0a+Ph5SppddRcXTH6igX+W5x7wK+VDsLefiAC2cHRG4DjEL2mpoVYrkDUPNQRaf+X4GTEKHtTzjvNw==", - "dev": true, + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.0.4.tgz", + "integrity": "sha512-zHhBXdvovjCXkxWA/542DfRd1dc4xbXQWkI8TYQARd1SwuuCNHMVUY3Cll4TWGFQthywRMxyP70BTHIm4XBVgg==", "dependencies": { - "@babel/core": "7.24.4", + "@babel/core": "7.24.7", "@types/babel__core": "7.20.5", "fast-glob": "3.3.2", "yargs": "^17.2.1" @@ -642,28 +672,84 @@ "localize-translate": "tools/bundles/src/translate/cli.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.0.0", - "@angular/compiler-cli": "18.0.0" + "@angular/compiler": "18.0.4", + "@angular/compiler-cli": "18.0.4" } }, - "node_modules/@angular/platform-browser": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.0.0.tgz", - "integrity": "sha512-fOqXQn15H33xGTGgNBUwXAg5KRpqcdsVfipFBuD1GMbjMLQAx/AagxsBavRiq3mKEdHZyQ+hI4mvaKQWOPKUOQ==", + "node_modules/@angular/localize/node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/animations": "18.0.0", - "@angular/common": "18.0.0", - "@angular/core": "18.0.0" - }, + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "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/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==" + }, + "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==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/platform-browser": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.0.4.tgz", + "integrity": "sha512-8TJEPzIRV89s1ZP9T+7g9K7PFNfec+4Xyw5BLaTRBOqjXHmMzk+miRx0L18Lr66rp5r2vbNEE9vojMVHQRwhVA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "18.0.4", + "@angular/common": "18.0.4", + "@angular/core": "18.0.4" + }, "peerDependenciesMeta": { "@angular/animations": { "optional": true @@ -671,65 +757,64 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.0.0.tgz", - "integrity": "sha512-Z7Y2qzEuFgCrkgcKPuyHGStEnZ89L3gr3SIgqoVlz4kauf0Fa70H6dxyd/RXV61OZwLXx0yt9rV5d8v+Ay+3fQ==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.0.4.tgz", + "integrity": "sha512-K36/gamqs8etGlmWew7IwZ/bDJdI5ZeUqvOUmkKjJ9F2I/g5P/zZrB1qExwN/zsxzxd9idkvEhwY+YDeiZEEJg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.0", - "@angular/compiler": "18.0.0", - "@angular/core": "18.0.0", - "@angular/platform-browser": "18.0.0" + "@angular/common": "18.0.4", + "@angular/compiler": "18.0.4", + "@angular/core": "18.0.4", + "@angular/platform-browser": "18.0.4" } }, "node_modules/@angular/platform-server": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-18.0.0.tgz", - "integrity": "sha512-xn/E1zYEWnvoeSGDcMjxOmUhOIkTQ4wSmoAEr3lNt8znB/+K3PnMsV6sHPSgOkfjzXuX7PFhW2tgvp4TbMgfbA==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-18.0.4.tgz", + "integrity": "sha512-kRyVIKafkvmG0zsYzw/uTxgEhBTpZUEjCNVM118VKweC6Ttx0mLNCERNP0FYC7z0P1ve3Hx2ifufZ33eIUGfEg==", "dependencies": { "tslib": "^2.3.0", "xhr2": "^0.2.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.0.0", - "@angular/common": "18.0.0", - "@angular/compiler": "18.0.0", - "@angular/core": "18.0.0", - "@angular/platform-browser": "18.0.0" + "@angular/animations": "18.0.4", + "@angular/common": "18.0.4", + "@angular/compiler": "18.0.4", + "@angular/core": "18.0.4", + "@angular/platform-browser": "18.0.4" } }, "node_modules/@angular/router": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.0.0.tgz", - "integrity": "sha512-bytfTypkJbHDv2QkD8jT2w63DWKicSYi5l7N+LPukb9/0pl3XYXKJ8cjlVLbiFvoo5Oz2oBFWYFucWsaPqDw3A==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.0.4.tgz", + "integrity": "sha512-nr1ZI3lynKBtr3a75APuVkIaiXRG5mEnW/RIyxwzxbKBB14901mby46o0jm9Y/CPb2rH5UpuwZhTKRE6QS/xLw==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.0", - "@angular/core": "18.0.0", - "@angular/platform-browser": "18.0.0", + "@angular/common": "18.0.4", + "@angular/core": "18.0.4", + "@angular/platform-browser": "18.0.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -737,30 +822,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", - "dev": true, + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -778,14 +861,12 @@ "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==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "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==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -794,7 +875,6 @@ "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", - "dev": true, "dependencies": { "@babel/types": "^7.24.5", "@jridgewell/gen-mapping": "^0.3.5", @@ -818,25 +898,25 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -849,25 +929,24 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "semver": "^6.3.1" }, "engines": { @@ -877,6 +956,30 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.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", @@ -887,12 +990,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", "regexpu-core": "^5.3.1", "semver": "^6.3.1" }, @@ -903,6 +1006,18 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.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", @@ -929,74 +1044,74 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, + "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==", + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1005,36 +1120,47 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1043,15 +1169,27 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1061,24 +1199,25 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1097,67 +1236,62 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", - "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.23.0", - "@babel/template": "^7.24.0", - "@babel/types": "^7.24.5" + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1167,10 +1301,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1179,13 +1312,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz", - "integrity": "sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1195,12 +1328,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.1", - "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.24.1.tgz", - "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "version": "7.24.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.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1210,14 +1343,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", - "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1227,13 +1360,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", - "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1318,12 +1451,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", - "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1333,12 +1466,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", - "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1490,12 +1623,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", - "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1540,12 +1673,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", - "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1555,12 +1688,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", - "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1570,13 +1703,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", - "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1586,13 +1719,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", - "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -1603,18 +1736,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", - "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "globals": "^11.1.0" }, "engines": { @@ -1624,14 +1757,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", - "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1641,12 +1798,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", - "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1656,13 +1813,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", - "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1672,12 +1829,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", - "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1687,12 +1844,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", - "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1703,13 +1860,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", - "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1719,12 +1876,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", - "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1735,13 +1892,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", - "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1751,14 +1908,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", - "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1768,12 +1925,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", - "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -1784,12 +1941,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", - "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1799,12 +1956,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", - "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1815,12 +1972,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", - "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1830,13 +1987,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", - "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1846,14 +2003,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1863,15 +2020,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", - "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1881,13 +2038,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", - "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1897,13 +2054,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1913,12 +2070,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", - "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1928,12 +2085,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", - "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -1944,12 +2101,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", - "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -1960,15 +2117,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", - "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.5" + "@babel/plugin-transform-parameters": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1978,13 +2135,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", - "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1994,12 +2151,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", - "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -2010,13 +2167,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", - "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -2027,12 +2184,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", - "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2042,13 +2199,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", - "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2058,14 +2215,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", - "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -2075,13 +2232,25 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", - "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2091,12 +2260,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", - "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2107,12 +2276,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", - "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2151,12 +2320,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", - "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2166,13 +2335,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", - "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2182,12 +2351,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", - "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2197,12 +2366,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", - "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2212,12 +2381,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", - "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2227,12 +2396,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", - "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2242,13 +2411,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", - "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2258,13 +2427,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", - "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2274,13 +2443,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", - "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2426,33 +2595,31 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", - "dev": true, + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2460,20 +2627,50 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", - "dev": true, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "optional": true + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2881,9 +3078,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.2.tgz", - "integrity": "sha512-4F1MBwVr3c/m4bAUef6LgkvBfSjzwH+OfldgHqcuacWwSUetFebM2wi58WfG9uk1rR98U6GwLed4asLJbwdV5w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", + "integrity": "sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==", "dev": true, "engines": { "node": ">=18" @@ -2985,22 +3182,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -3010,11 +3191,21 @@ "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==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "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==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -3028,7 +3219,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -3037,7 +3227,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -3055,14 +3244,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "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==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3107,9 +3294,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.1.3.tgz", - "integrity": "sha512-g//kkF4kOwUjemValCtOc/xiYzmwMRmWq3Bn+YnzOzuZLHq2PpMOxxIayN3cKbo7Ko2Np65t6D9H81IvXbXhqg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.2.0.tgz", + "integrity": "sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg==", "dev": true, "engines": { "node": ">=10.0" @@ -3219,9 +3406,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", - "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", "cpu": [ "arm64" ], @@ -3232,9 +3419,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", - "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", "cpu": [ "x64" ], @@ -3245,9 +3432,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", - "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", "cpu": [ "arm" ], @@ -3258,9 +3445,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", - "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", "cpu": [ "arm64" ], @@ -3271,9 +3458,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", - "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", "cpu": [ "x64" ], @@ -3284,9 +3471,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", - "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", "cpu": [ "x64" ], @@ -3296,10 +3483,26 @@ "win32" ] }, + "node_modules/@ng-bootstrap/ng-bootstrap": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-17.0.0.tgz", + "integrity": "sha512-hTbBtozJlpevF1RO6J2adCoXiAkMTPV3wmXIyK05dVha4VsKjHibgaL6YldToKoh6ElQnIYkPEIJHX9z5EtyMw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "@angular/localize": "^18.0.0", + "@popperjs/core": "^2.11.8", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@ngtools/webpack": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.0.tgz", - "integrity": "sha512-wcJp15H52RgEiZOcq/8YlgF53dNR2C+ap6mof8HziD5lTXmkPLKn1US0gqHixu5njkWKslCzu2td/k2Fg6r5Kg==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.5.tgz", + "integrity": "sha512-Dx386WZZn0RwUaBHQYhDW8oi254SxEu8Ty5LHnStqBP6xXdcnsdGel+h9qvJ67He9iu8Rj0PB64EFE4PiklMdQ==", "dev": true, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", @@ -3343,7 +3546,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -3356,7 +3558,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -3365,7 +3566,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -3489,9 +3689,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.0.tgz", - "integrity": "sha512-1aL4TuVrLS9sf8quCLerU3H9J4vtCtgu8VauYozrmEyU57i/EdKleCnsQ7vpnABIH6c9mnTxcH5sFkO3BlV8wQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.0.tgz", + "integrity": "sha512-qe/kiqqkW0AGtvBjL8TJKZk/eBBSpnJkUWvHdQ9jM2lKHXRYYJuyNpJPlJw3c8QjC2ow6NZYiLExhUaeJelbxQ==", "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", @@ -3506,52 +3706,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@npmcli/package-json/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@npmcli/promise-spawn": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", @@ -3589,9 +3743,9 @@ } }, "node_modules/@npmcli/redact": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.0.tgz", - "integrity": "sha512-SEjCPAVHWYUIQR+Yn03kJmrJjZDtJLYpj300m3HV9OTRZNpC5YpbMsM3eTkECyT4aWj8lDr9WeY6TWefpubtYQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", "dev": true, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -3638,6 +3792,217 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@nrwl/devkit": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-19.4.1.tgz", + "integrity": "sha512-BVo735k+HgCQ78fHi/yDFN7n0kUbCujyASm+iu6BKLi0b2aPi9Dw+Igztiv38g/Gyjjapos0O39XLpbcoGnw3Q==", + "dependencies": { + "@nx/devkit": "19.4.1" + } + }, + "node_modules/@nrwl/tao": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.4.1.tgz", + "integrity": "sha512-4PHs6Ja8PkWkIrg8ViB47j+dR2fDn51vtQTWL33n4q5hqZ65rvsMHNch4UsC52XUSv55IZnJwcYlxhAx/vXk3g==", + "dependencies": { + "nx": "19.4.1", + "tslib": "^2.3.0" + }, + "bin": { + "tao": "index.js" + } + }, + "node_modules/@nx/devkit": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-19.4.1.tgz", + "integrity": "sha512-vOUes8e8guFmbcpUcppUlx120Y52ovY46ZnKogOjnw5q7LN12Fvn68A2wBF8SYmyiYmPd56YtUV7A6LuS8Wd3w==", + "dependencies": { + "@nrwl/devkit": "19.4.1", + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "minimatch": "9.0.3", + "semver": "^7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" + }, + "peerDependencies": { + "nx": ">= 17 <= 20" + } + }, + "node_modules/@nx/devkit/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nx/devkit/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.4.1.tgz", + "integrity": "sha512-WfNRFpMoBB5Ayzvwqfy+anEUgqOZLnLctGG1qwMhCOqczcPUtuTrAjRilMYZ7RrT0cvw0da8dTkpkAsAURS7Ig==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.4.1.tgz", + "integrity": "sha512-p8/lJZLeqAFjCyINrQUvlUvG2GkWN0IlqRm7NknNFXisFDwzcT6u12GR96hPbl+6eVBOtldYhwlufF4tZQDJiw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.4.1.tgz", + "integrity": "sha512-qi/tRWKuFS6wpYbAD/s0SBqh/2pNXNg+ytxmon3czYPuUrIiMfmXGxtz922P6YUSOWtL2N6Q9UI6vqZwS+g9/A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.4.1.tgz", + "integrity": "sha512-AIowQrN14ucZnBr4Syo2oDGYLqjuJHSGgY/ur6mPoxH02ghGAd68Mc1swX8elGRgBcGc251s05H8MjyPQVsT3A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.4.1.tgz", + "integrity": "sha512-TG/GfX7olq8bINKLOfamikHJWchYapcJheHj7aUZo951X96s6jYpbeZjwGrVesTJ2fO6EYlS7T1sJIqMoSMxaw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.4.1.tgz", + "integrity": "sha512-GBBKbERw0baa4JKTbQi8LAERI6C5n3Scrk76pmzCn0HW5GxaQygr61kg6H6C7Duy+w+3D7vwMxCk2wPbUOTuOA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.4.1.tgz", + "integrity": "sha512-zaHHFM75hLVfMEBR8U7X8xiND1HNQJxybItuoBpnXHVRfKJwp1quByqArnaKKCzsvLvO5HdoXIA80ToJNmDkBQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.4.1.tgz", + "integrity": "sha512-ygfqznUMoXnrI23U12VwkxOqG4C7sV85YaF7fWDIMuszxYU7KtrVAQ5YG0LNW5KNa1JCgKkjL9YszEiNJxK47Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.4.1.tgz", + "integrity": "sha512-tOpjieJ7XqbhvgQX9xcKTu/nWvj+w9tL0j6NlpP5Gkq1LiGUuXG2EWvOEGS5CsyAtT/tncLo2OJUx//Ah+dEtw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.4.1.tgz", + "integrity": "sha512-u9h7nrIplf79A6Yhzk1ZlNNlHrhuKrDaGMyhpTx3QaLEiRp0Kl3haMrnYmPlpRFNDwWXWDKzwiTWZtQoo2JoaA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3658,9 +4023,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", "cpu": [ "arm" ], @@ -3671,9 +4036,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", "cpu": [ "arm64" ], @@ -3684,9 +4049,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", "cpu": [ "arm64" ], @@ -3697,9 +4062,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", "cpu": [ "x64" ], @@ -3710,9 +4075,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", "cpu": [ "arm" ], @@ -3723,9 +4088,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", "cpu": [ "arm" ], @@ -3736,9 +4101,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", "cpu": [ "arm64" ], @@ -3749,9 +4114,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", "cpu": [ "arm64" ], @@ -3762,9 +4127,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", "cpu": [ "ppc64" ], @@ -3775,9 +4140,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", "cpu": [ "riscv64" ], @@ -3788,9 +4153,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", "cpu": [ "s390x" ], @@ -3801,9 +4166,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", "cpu": [ "x64" ], @@ -3814,9 +4179,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", "cpu": [ "x64" ], @@ -3827,9 +4192,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", "cpu": [ "arm64" ], @@ -3840,9 +4205,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", "cpu": [ "ia32" ], @@ -3853,9 +4218,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", "cpu": [ "x64" ], @@ -3866,13 +4231,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.0.0.tgz", - "integrity": "sha512-cFah74mKIg+mCGur1Q1BmsQ/u+Ne/0MOwIxe2oYSlzDpktOuKAUItPFe4GHxm9Mu5qZzOX0Z4RRnSojU8XgZEw==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.0.5.tgz", + "integrity": "sha512-dV50GIEGl6S5wE6xtAhmHWdLhsOlnNUpAx/v3BPR2AOr90zJvIM03TqAQTzAlnPatxK2WLelRgqVMbPfAVvLAg==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.0.0", - "@angular-devkit/schematics": "18.0.0", + "@angular-devkit/core": "18.0.5", + "@angular-devkit/schematics": "18.0.5", "jsonc-parser": "3.2.1" }, "engines": { @@ -3955,14 +4320,44 @@ "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==" + }, "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 }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", + "node_modules/@ts-morph/common": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.23.0.tgz", + "integrity": "sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==", + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, @@ -3983,35 +4378,10 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@tufjs/models/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tufjs/models/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -4024,7 +4394,6 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -4033,7 +4402,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -4043,7 +4411,6 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, "dependencies": { "@babel/types": "^7.20.7" } @@ -4110,6 +4477,36 @@ "@types/node": "*" } }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "optional": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "optional": true + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "optional": true + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "optional": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -4149,9 +4546,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.1.tgz", - "integrity": "sha512-ej0phymbFLoCB26dbbq5PGScsf2JAJ4IJHjG10LalgUV36XKTmA4GdA+PVllKvRk0sEKt64X8975qFnkSi0hqA==", + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", "dev": true, "dependencies": { "@types/node": "*", @@ -4206,6 +4603,15 @@ "log4js": "^6.4.1" } }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "optional": true, + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4227,10 +4633,16 @@ "@types/node": "*" } }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "optional": true + }, "node_modules/@types/node": { - "version": "18.19.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", - "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", + "version": "18.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", + "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -4307,6 +4719,12 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "dev": true }, + "node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "optional": true + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -4489,8 +4907,55 @@ "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 + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + }, + "node_modules/@yarnpkg/parsers": { + "version": "3.0.0-rc.46", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", + "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@yarnpkg/parsers/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/@yarnpkg/parsers/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==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@yarnpkg/parsers/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/@zkochan/js-yaml": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", + "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } }, "node_modules/abbrev": { "version": "2.0.0", @@ -4515,9 +4980,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4535,6 +5000,16 @@ "acorn": "^8" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", @@ -4604,9 +5079,9 @@ } }, "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==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "dependencies": { "ajv": "^8.0.0" @@ -4636,7 +5111,6 @@ "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, "engines": { "node": ">=6" } @@ -4672,7 +5146,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4681,7 +5154,6 @@ "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==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -4693,7 +5165,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -4706,7 +5177,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -4715,13 +5185,9 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -4732,15 +5198,12 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, "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, - "peer": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { "version": "10.4.19", @@ -4779,6 +5242,16 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-loader": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", @@ -4796,22 +5269,6 @@ "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, - "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-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -4863,14 +5320,12 @@ "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 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "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", @@ -4914,7 +5369,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "engines": { "node": ">=8" }, @@ -4926,7 +5380,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -5022,20 +5475,17 @@ ] }, "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, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "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==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -5044,10 +5494,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "funding": [ { "type": "opencollective", @@ -5063,10 +5512,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -5079,7 +5528,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -5152,37 +5600,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/cacache/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/lru-cache": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", @@ -5192,21 +5609,6 @@ "node": "14 || >=16.14" } }, - "node_modules/cacache/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5235,20 +5637,10 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001621", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", - "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", - "dev": true, + "version": "1.0.30001636", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", + "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", "funding": [ { "type": "opencollective", @@ -5268,7 +5660,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5278,6 +5669,16 @@ "node": ">=4" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -5288,7 +5689,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5318,9 +5718,9 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "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, "engines": { "node": ">=6.0" @@ -5339,7 +5739,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, "dependencies": { "restore-cursor": "^3.1.0" }, @@ -5368,11 +5767,21 @@ "node": ">= 12" } }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "optional": true, + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -5386,7 +5795,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5401,7 +5809,6 @@ "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, "dependencies": { "color-name": "~1.1.4" }, @@ -5412,14 +5819,12 @@ "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==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "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==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5436,7 +5841,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, "engines": { "node": ">=0.8" } @@ -5467,6 +5871,11 @@ "node": ">=6" } }, + "node_modules/code-block-writer": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.1.tgz", + "integrity": "sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==" + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -5481,7 +5890,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -5489,8 +5897,7 @@ "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==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/color-string": { "version": "1.9.1", @@ -5522,8 +5929,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5606,8 +6011,7 @@ "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 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/connect": { "version": "3.7.0", @@ -5672,8 +6076,7 @@ "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==", - "dev": true + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { "version": "0.4.2", @@ -5770,6 +6173,15 @@ "node": ">= 0.10" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -5796,24 +6208,6 @@ } } }, - "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 - }, - "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, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/critters": { "version": "0.0.22", "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", @@ -5913,21 +6307,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/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, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-loader": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.1.tgz", @@ -6016,240 +6395,796 @@ "node": ">=18" } }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "peer": true + }, "node_modules/custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", "dev": true }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "peer": true, - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, + "node_modules/cytoscape": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.0.tgz", + "integrity": "sha512-l590mjTHT6/Cbxp13dGPC2Y7VXdgc+rUeF8AnF/JPzhjNevbDJfObnJgaSjlldOgBQZbue+X6IUZ7r5GAgvauQ==", + "optional": true, "engines": { - "node": ">=18" + "node": ">=0.10" } }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "engines": { - "node": ">=4.0" + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "optional": true, "dependencies": { - "ms": "2.1.2" + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=12" } }, - "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, - "peer": true - }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "optional": true, "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "internmap": "1 - 2" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "dev": true, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "optional": true, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "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, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "optional": true, "dependencies": { - "execa": "^5.0.0" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" }, "engines": { - "node": ">= 10" + "node": ">=12" } }, - "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, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "optional": true, "dependencies": { - "clone": "^1.0.2" + "d3-path": "1 - 3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=12" } }, - "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, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "optional": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "d3-array": "^3.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "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, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "optional": true, + "dependencies": { + "delaunator": "5" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "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, - "peer": true, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "optional": true, "engines": { - "node": ">=0.4.0" + "node": ">=12" } }, - "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, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=12" } }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, + "node_modules/d3-dsv/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==", + "optional": true, "engines": { - "node": ">=8" + "node": ">= 10" } }, - "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 + "node_modules/d3-dsv/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==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "optional": true, + "engines": { + "node": ">=12" + } }, - "node_modules/dir-glob": { + "node_modules/d3-fetch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "optional": true, "dependencies": { - "path-type": "^4.0.0" + "d3-dsv": "1 - 3" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "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, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "optional": true, "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", - "dev": true, - "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "optional": true, + "engines": { + "node": ">=12" } }, - "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, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "optional": true, "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "d3-array": "2.5.0 - 3" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": ">=12" } }, - "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, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "optional": true, + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "optional": true, + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "optional": true + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "optional": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "optional": true + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "optional": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "optional": true, + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "optional": true, + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "peer": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "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, + "peer": true + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "optional": true, + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "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==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "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 + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "optional": true, + "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==", + "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, + "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, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.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, + "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", @@ -6272,6 +7207,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "optional": true + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -6286,6 +7227,36 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", + "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", + "dependencies": { + "dotenv": "^16.4.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6298,17 +7269,41 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.4.777", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.777.tgz", - "integrity": "sha512-n02NCwLJ3wexLfK/yQeqfywCblZqLcXphzmid5e8yVPdtEcida7li0A5WQKghHNG0FeOMCzeFOzEbtAh5riXFw==", - "dev": true + "version": "1.4.810", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz", + "integrity": "sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==" + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "optional": true }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emoji-toolkit": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-8.0.0.tgz", + "integrity": "sha512-Vz8YIqQJsQ+QZ4yuKMMzliXceayqfWbNjb6bST+vm77QAhU2is3I+/PRxrNknW+q1bvHHMgjLCQXxzINWLVapg==", + "optional": true }, "node_modules/emojis-list": { "version": "3.0.0", @@ -6357,10 +7352,18 @@ "node": ">=0.10.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", - "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -6372,7 +7375,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" @@ -6388,9 +7391,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", - "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -6400,11 +7403,28 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", - "dev": true + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/entities": { "version": "4.5.0", @@ -6536,7 +7556,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, "engines": { "node": ">=6" } @@ -6551,7 +7570,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -6573,7 +7591,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -6668,6 +7685,12 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -6797,7 +7820,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -6819,7 +7841,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -6842,11 +7863,43 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "dev": true }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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==", + "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==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6916,23 +7969,25 @@ } }, "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==", + "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, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" }, "engines": { - "node": ">=8" + "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, "bin": { "flat": "cli.js" } @@ -6952,7 +8007,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -6969,9 +8023,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -6984,24 +8038,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/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, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7042,6 +8082,44 @@ "node": ">= 0.6" } }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "dependencies": { + "js-yaml": "^3.13.1" + } + }, + "node_modules/front-matter/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/front-matter/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==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/front-matter/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/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -7077,7 +8155,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -7100,7 +8177,6 @@ "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==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -7109,7 +8185,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7133,15 +8208,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7155,20 +8221,23 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7178,7 +8247,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -7196,7 +8264,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "engines": { "node": ">=4" } @@ -7220,6 +8287,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "optional": true, + "dependencies": { + "delegate": "^3.1.2" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -7247,7 +8323,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -7563,7 +8638,6 @@ "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", @@ -7583,7 +8657,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "engines": { "node": ">= 4" } @@ -7600,30 +8673,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ignore-walk/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -7659,15 +8708,6 @@ "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, - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -7690,6 +8730,7 @@ "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, "dependencies": { "once": "^1.3.0", @@ -7699,8 +8740,7 @@ "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 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "4.1.2", @@ -7749,6 +8789,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -7762,12 +8811,6 @@ "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 - }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -7787,7 +8830,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -7796,12 +8838,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7811,7 +8856,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, "bin": { "is-docker": "cli.js" }, @@ -7826,7 +8870,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7835,7 +8878,6 @@ "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, "engines": { "node": ">=8" } @@ -7844,7 +8886,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -7889,7 +8930,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, "engines": { "node": ">=8" } @@ -7916,7 +8956,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -7977,7 +9016,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, "engines": { "node": ">=10" }, @@ -7995,7 +9033,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -8046,28 +9083,19 @@ } }, "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==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dev": true, "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "semver": "^7.5.4" }, "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, - "bin": { - "semver": "bin/semver.js" + "node": ">=10" } }, "node_modules/istanbul-lib-report": { @@ -8142,9 +9170,9 @@ } }, "node_modules/jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -8159,6 +9187,107 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "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==", + "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==", + "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==", + "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==", + "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==" + }, + "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==", + "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==", + "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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.1.0.tgz", @@ -8178,50 +9307,90 @@ "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "dev": true }, - "node_modules/jasmine/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, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dependencies": { - "balanced-match": "^1.0.0" + "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/jasmine/node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", - "dev": true, + "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==", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "color-convert": "^2.0.1" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "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==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "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==", + "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==" + }, + "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==", + "engines": { + "node": ">=8" } }, - "node_modules/jasmine/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, + "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==", "dependencies": { - "brace-expansion": "^2.0.1" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -8263,9 +9432,9 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -8274,17 +9443,15 @@ "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==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "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==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -8306,9 +9473,9 @@ "dev": true }, "node_modules/jsdom": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", - "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", "dev": true, "peer": true, "dependencies": { @@ -8317,21 +9484,21 @@ "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", + "nwsapi": "^2.2.10", "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "rrweb-cssom": "^0.7.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.3", + "tough-cookie": "^4.1.4", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.16.0", + "ws": "^8.17.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -8346,33 +9513,10 @@ } } }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true, - "peer": true, - "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/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -8399,7 +9543,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -8438,6 +9581,14 @@ "node": "*" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/karma": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", @@ -8485,6 +9636,18 @@ "which": "^1.2.1" } }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/karma-coverage": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", @@ -8502,6 +9665,53 @@ "node": ">=10.0.0" } }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-coverage/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, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/karma-jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", @@ -8529,9 +9739,9 @@ } }, "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", "dev": true }, "node_modules/karma-jsdom-launcher": { @@ -8576,6 +9786,49 @@ "winston": "^3.0.0" } }, + "node_modules/karma-sonarqube-reporter/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-sonarqube-reporter/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, + "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/karma-sonarqube-reporter/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/karma-sonarqube-reporter/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -8624,29 +9877,48 @@ } }, "node_modules/karma-webpack/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==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/karma-webpack/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "node_modules/karma-webpack/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, "dependencies": { - "brace-expansion": "^2.0.1" + "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": ">=16 || 14 >=14.17" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/karma-webpack/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/karma-webpack/node_modules/webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", @@ -8671,6 +9943,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -8700,6 +9982,39 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/karma/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, + "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/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/karma/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8762,6 +10077,37 @@ "node": ">=10" } }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8771,6 +10117,15 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -8778,15 +10133,21 @@ "dev": true }, "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.0.tgz", + "integrity": "sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==", "dev": true, "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.8.1" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "optional": true + }, "node_modules/less": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", @@ -8953,15 +10314,18 @@ } }, "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==", + "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, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -8970,6 +10334,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "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==", + "optional": true + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -8980,7 +10350,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -8996,7 +10365,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -9011,7 +10379,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9027,7 +10394,6 @@ "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, "dependencies": { "color-name": "~1.1.4" }, @@ -9038,14 +10404,12 @@ "node_modules/log-symbols/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 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/log-symbols/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, "engines": { "node": ">=8" } @@ -9054,7 +10418,6 @@ "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, "dependencies": { "has-flag": "^4.0.0" }, @@ -9094,142 +10457,681 @@ "node": ">= 12.0.0" } }, - "node_modules/logform/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, - "engines": { - "node": ">=0.1.90" + "node_modules/logform/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, + "engines": { + "node": ">=0.1.90" + } + }, + "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==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "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/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "optional": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "optional": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.9.3.tgz", + "integrity": "sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.1.2", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "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 + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz", + "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "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==", - "dev": true, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, "dependencies": { - "yallist": "^3.0.2" + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "micromark-util-symbol": "^1.0.0" } }, - "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, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "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, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, "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" + "micromark-util-symbol": "^1.0.0" } }, - "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, - "engines": { - "node": ">= 0.6" + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-types": "^1.0.0" } }, - "node_modules/memfs": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.9.2.tgz", - "integrity": "sha512-f16coDZlTG1jskq3mxarwB+fGRrd0uXWt+o1WIhRfOwbXQZqUDsTVxQBFK9JjRQHblg8eAG2JSbprDXKjc7ijQ==", - "dev": true, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, "dependencies": { - "@jsonjoy.com/json-pack": "^1.0.3", - "@jsonjoy.com/util": "^1.1.2", - "sonic-forest": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">= 4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true - }, - "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 - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true, + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "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, - "engines": { - "node": ">= 0.6" - } + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "optional": true }, "node_modules/micromatch": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.6.tgz", - "integrity": "sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ==", - "dev": true, + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dependencies": { "braces": "^3.0.3", - "picomatch": "^4.0.2" + "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==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -9246,7 +11148,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -9255,7 +11156,6 @@ "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, "dependencies": { "mime-db": "1.52.0" }, @@ -9267,7 +11167,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "engines": { "node": ">=6" } @@ -9299,30 +11198,31 @@ "dev": true }, "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, + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "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, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -9353,51 +11253,23 @@ "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, - "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, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "optionalDependencies": { + "encoding": "^0.1.13" } }, - "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 - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "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, "dependencies": { - "jsonparse": "^1.3.1", "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/minipass-json-stream/node_modules/minipass": { + "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==", @@ -9409,7 +11281,7 @@ "node": ">=8" } }, - "node_modules/minipass-json-stream/node_modules/yallist": { + "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==", @@ -9518,6 +11390,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "optional": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -9542,33 +11423,36 @@ } }, "node_modules/msgpackr-extract": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", - "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { - "node-gyp-build-optional-packages": "5.0.7" + "node-gyp-build-optional-packages": "5.2.2" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", - "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "dev": true, "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", @@ -9660,6 +11544,67 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ngx-cookie-service": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-18.0.0.tgz", + "integrity": "sha512-hkkUckzZTXXWtFgvVkT2hg6mwYMLXioXDZWBsVCOy9gYkADjsj0N5VViO7eo2izQ0VcMPd/Etog1trf/T4oZMQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/common": "^18.0.0-rc.0", + "@angular/core": "^18.0.0-rc.0" + } + }, + "node_modules/ngx-markdown": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-18.0.0.tgz", + "integrity": "sha512-sFR9dIOKobdhNKZTlCrX3RmpoAhZ7k3T9h7oWJP676Oe9BsoxuAYZKJmFDT20vrY6xmFD3WtLJDZR7rNRLf6Uw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^8.0.0", + "katex": "^0.16.0", + "mermaid": "^10.6.0", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/platform-browser": "^18.0.0", + "marked": ">= 9.0.0 < 13.0.0", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/ngxtension": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/ngxtension/-/ngxtension-3.5.5.tgz", + "integrity": "sha512-LU0YtjLllTZ+GLs81HB+afBerLF7dJ/sgz5R4sh32lP9NHAuTJzRsJbkfwZUd/UErJlUPNPZn8WXxTnWTqefQw==", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "^18.0.1", + "@nx/devkit": "^19.0.0", + "nx": "^19.0.0", + "ts-morph": "^22.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@use-gesture/vanilla": "^10.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@use-gesture/vanilla": { + "optional": true + } + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -9747,37 +11692,6 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -9787,21 +11701,6 @@ "node": ">=16" } }, - "node_modules/node-gyp/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/node-gyp/node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -9826,11 +11725,21 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "optional": true }, "node_modules/nopt": { "version": "7.2.1", @@ -9866,7 +11775,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9956,16 +11864,16 @@ } }, "node_modules/npm-registry-fetch": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.0.1.tgz", - "integrity": "sha512-fLu9MTdZTlJAHUek/VLklE6EpIiP3VZpTiuN7OOMCt2Sd67NCpSEetMaxHHEZiZxllp8ZLsUpvbEszqTFEc+wA==", + "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, "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", - "minipass-json-stream": "^1.0.1", "minizlib": "^2.1.2", "npm-package-arg": "^11.0.0", "proc-log": "^4.0.0" @@ -9978,7 +11886,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "dependencies": { "path-key": "^3.0.0" }, @@ -9994,16 +11901,249 @@ "dependencies": { "boolbase": "^1.0.0" }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true, + "peer": true + }, + "node_modules/nx": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/nx/-/nx-19.4.1.tgz", + "integrity": "sha512-II+Ix/z6i1E2/3DinwnYKWlM0g3bO/1bcQkwhpaPee5GwHejBYdxlQ2B9uxwqRMYgpF5tFJr/0Q8WsBQybuSJw==", + "hasInstallScript": true, + "dependencies": { + "@nrwl/tao": "19.4.1", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.0-rc.46", + "@zkochan/js-yaml": "0.0.7", + "axios": "^1.6.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "front-matter": "^4.0.2", + "fs-extra": "^11.1.0", + "ignore": "^5.0.4", + "jest-diff": "^29.4.1", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "semver": "^7.5.3", + "string-width": "^4.2.3", + "strong-log-transformer": "^2.1.0", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js", + "nx-cloud": "bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "19.4.1", + "@nx/nx-darwin-x64": "19.4.1", + "@nx/nx-freebsd-x64": "19.4.1", + "@nx/nx-linux-arm-gnueabihf": "19.4.1", + "@nx/nx-linux-arm64-gnu": "19.4.1", + "@nx/nx-linux-arm64-musl": "19.4.1", + "@nx/nx-linux-x64-gnu": "19.4.1", + "@nx/nx-linux-x64-musl": "19.4.1", + "@nx/nx-win32-arm64-msvc": "19.4.1", + "@nx/nx-win32-x64-msvc": "19.4.1" + }, + "peerDependencies": { + "@swc-node/register": "^1.8.0", + "@swc/core": "^1.3.85" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nx/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/nx/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nx/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==" + }, + "node_modules/nx/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/nx/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/nx/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/nx/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/nx/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nx/node_modules/ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dependencies": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/nwsapi": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", - "dev": true, - "peer": true + "node_modules/nx/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/nx/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/object-assign": { "version": "4.1.1", @@ -10015,10 +12155,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10054,7 +12197,6 @@ "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, "dependencies": { "wrappy": "1" } @@ -10072,7 +12214,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -10087,7 +12228,6 @@ "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, "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -10209,30 +12349,33 @@ } }, "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==", + "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, "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "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==", + "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, "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-map": { @@ -10276,14 +12419,11 @@ "node": ">= 4" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true }, "node_modules/pacote": { "version": "18.0.6", @@ -10408,13 +12548,18 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "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==", + "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, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, "node_modules/path-is-absolute": { @@ -10430,7 +12575,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -10484,8 +12628,7 @@ "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "4.0.2", @@ -10533,76 +12676,6 @@ "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, - "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, - "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, - "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, - "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, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -10747,9 +12820,9 @@ "dev": true }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -10761,6 +12834,39 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", @@ -10817,6 +12923,11 @@ "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==" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -10832,13 +12943,10 @@ "peer": true }, "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, - "engines": { - "node": ">=6" - } + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true }, "node_modules/qjobs": { "version": "1.2.0", @@ -10875,7 +12983,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -10924,11 +13031,15 @@ "node": ">= 0.8" } }, + "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==" + }, "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, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10942,7 +13053,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -10954,7 +13064,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -10965,8 +13074,7 @@ "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==", - "dev": true + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "node_modules/regenerate": { "version": "1.4.2", @@ -11049,7 +13157,6 @@ "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==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11087,12 +13194,12 @@ } }, "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==", + "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, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/resolve-url-loader": { @@ -11138,7 +13245,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -11147,6 +13253,11 @@ "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==" + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -11160,21 +13271,21 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "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, "dependencies": { "glob": "^7.1.3" @@ -11186,10 +13297,59 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/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, + "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/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "optional": true + }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -11202,29 +13362,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", "fsevents": "~2.3.2" } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true, "peer": true }, @@ -11253,7 +13413,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11272,6 +13431,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "optional": true + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -11280,11 +13445,22 @@ "tslib": "^2.1.0" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "optional": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "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", @@ -11313,7 +13489,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sass": { "version": "1.77.2", @@ -11373,9 +13549,9 @@ } }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "optional": true }, @@ -11411,6 +13587,29 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/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, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "optional": true + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -11431,13 +13630,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -11445,24 +13640,6 @@ "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==", - "dev": true, - "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==", - "dev": true - }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -11706,10 +13883,16 @@ } }, "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 + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sigstore": { "version": "2.3.1", @@ -11784,13 +13967,13 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", - "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "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, "dependencies": { "debug": "~4.3.4", - "ws": "~8.11.0" + "ws": "~8.17.1" } }, "node_modules/socket.io-parser": { @@ -11845,25 +14028,6 @@ "node": ">= 14" } }, - "node_modules/sonic-forest": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sonic-forest/-/sonic-forest-1.0.3.tgz", - "integrity": "sha512-dtwajos6IWMEWXdEbW1IkEkyL2gztCAgDplRIX+OT5aRKnEd5e7r7YCxRgXZdhRP1FBdOBf8axeTPhzDv8T4wQ==", - "dev": true, - "dependencies": { - "tree-dump": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -11960,9 +14124,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/spdy": { @@ -11996,9 +14160,9 @@ } }, "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "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 }, "node_modules/ssri": { @@ -12048,7 +14212,6 @@ "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, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12057,7 +14220,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -12086,7 +14248,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12107,6 +14268,14 @@ "node": ">=8" } }, + "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==", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -12116,11 +14285,32 @@ "node": ">=6" } }, + "node_modules/strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", + "dependencies": { + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + }, + "bin": { + "sl-log-transformer": "bin/sl-log-transformer.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "optional": true + }, "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==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -12182,6 +14372,21 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -12334,20 +14539,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -12366,12 +14557,23 @@ "tslib": "^2" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, "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 }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12388,7 +14590,6 @@ "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==", - "dev": true, "engines": { "node": ">=4" } @@ -12397,7 +14598,6 @@ "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==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -12430,6 +14630,16 @@ "node": ">=6" } }, + "node_modules/tough-cookie/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, + "peer": true, + "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", @@ -12453,6 +14663,16 @@ "node": ">=18" } }, + "node_modules/tr46/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, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tree-dump": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.1.tgz", @@ -12487,10 +14707,41 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-morph": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-22.0.0.tgz", + "integrity": "sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==", + "dependencies": { + "@ts-morph/common": "~0.23.0", + "code-block-writer": "^13.0.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==", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tuf-js": { "version": "2.2.1", @@ -12541,7 +14792,6 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12551,9 +14801,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", - "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", "dev": true, "funding": [ { @@ -12651,6 +14901,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "optional": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -12672,7 +14935,6 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -12707,6 +14969,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js/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, + "engines": { + "node": ">=6" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -12721,8 +14992,7 @@ "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 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -12742,6 +15012,24 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "optional": true, + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, "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", @@ -13279,7 +15567,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, "dependencies": { "defaults": "^1.0.3" } @@ -13290,6 +15577,12 @@ "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", "dev": true }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "optional": true + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -13301,10 +15594,11 @@ } }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.92.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", + "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -13312,10 +15606,10 @@ "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -13435,15 +15729,6 @@ } } }, - "node_modules/webpack-dev-server/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -13456,28 +15741,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", @@ -13517,21 +15780,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", @@ -13568,27 +15816,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true, - "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/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -13638,6 +15865,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13654,6 +15882,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, + "peer": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -13662,19 +15891,22 @@ "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 + "dev": true, + "peer": true }, "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 + "dev": true, + "peer": true }, "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, + "peer": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -13762,15 +15994,18 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/wildcard": { @@ -13925,20 +16160,19 @@ "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 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -13993,7 +16227,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -14001,13 +16234,12 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", "bin": { "yaml": "bin.mjs" }, @@ -14019,7 +16251,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -14037,7 +16268,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -14055,9 +16285,9 @@ } }, "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==" + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz", + "integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==" } } } diff --git a/marketplace-ui/package.json b/marketplace-ui/package.json index f742e45fb..0638ea314 100644 --- a/marketplace-ui/package.json +++ b/marketplace-ui/package.json @@ -20,11 +20,18 @@ "@angular/platform-server": "^18.0.0", "@angular/router": "^18.0.0", "@fortawesome/fontawesome-free": "^6.5.2", + "@ng-bootstrap/ng-bootstrap": "^17.0.0", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", + "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "jwt-decode": "^4.0.0", "karma-viewport": "^1.0.9", + "marked": "^12.0.0", + "ngx-markdown": "^18.0.0", + "ngx-cookie-service": "^18.0.0", + "ngxtension": "^3.5.5", "rxjs": "~7.8.0", "tslib": "^2.3.0", "yaml": "^2.4.2", diff --git a/marketplace-ui/sonar-project.properties b/marketplace-ui/sonar-project.properties index e2453c704..432bf0ea7 100644 --- a/marketplace-ui/sonar-project.properties +++ b/marketplace-ui/sonar-project.properties @@ -2,4 +2,4 @@ sonar.sources=src sonar.tests=src sonar.exclusions=**/node_modules/**, src/assets/**, **/*.html, **/*.scss, src/app/shared/mocks/**, **/*.constant.ts, **/*.enum.ts, **/*.routes.ts, **/*.model.ts, **/*.config.ts, src/environments/** sonar.test.inclusions=**/*.spec.ts -sonar.typescript.lcov.reportPaths=coverage/lcov.info \ No newline at end of file +sonar.typescript.lcov.reportPaths=coverage/lcov.info diff --git a/marketplace-ui/src/app/app.config.ts b/marketplace-ui/src/app/app.config.ts index 14db020f5..5905267e0 100644 --- a/marketplace-ui/src/app/app.config.ts +++ b/marketplace-ui/src/app/app.config.ts @@ -1,23 +1,16 @@ -import { - HttpClient, - provideHttpClient, - withFetch, - withInterceptors -} from '@angular/common/http'; -import { - ApplicationConfig, - importProvidersFrom, - provideExperimentalZonelessChangeDetection -} from '@angular/core'; +import { HttpClient, provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; +import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { routes } from './app.routes'; +import { MARKED_OPTIONS, MarkdownModule } from 'ngx-markdown'; +import { markedOptionsFactory } from './core/configs/markdown.config'; import { httpLoaderFactory } from './core/configs/translate.config'; import { apiInterceptor } from './core/interceptors/api.interceptor'; export const appConfig: ApplicationConfig = { providers: [ - provideExperimentalZonelessChangeDetection(), + provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withFetch(), withInterceptors([apiInterceptor])), importProvidersFrom( @@ -28,6 +21,14 @@ export const appConfig: ApplicationConfig = { deps: [HttpClient] } }) + ), + importProvidersFrom( + MarkdownModule.forRoot({ + markedOptions: { + provide: MARKED_OPTIONS, + useFactory: markedOptionsFactory + } + }) ) ] }; diff --git a/marketplace-ui/src/app/app.routes.ts b/marketplace-ui/src/app/app.routes.ts index 4b9644f26..76ebff774 100644 --- a/marketplace-ui/src/app/app.routes.ts +++ b/marketplace-ui/src/app/app.routes.ts @@ -1,14 +1,18 @@ import { Routes } from '@angular/router'; +import { GithubCallbackComponent } from './auth/github-callback/github-callback.component'; export const routes: Routes = [ { path: '', - loadChildren: () => - import('./modules/home/home.routes').then((m) => m.routes), + loadChildren: () => import('./modules/home/home.routes').then(m => m.routes) }, { path: ':id', loadChildren: () => - import('./modules/product/product.routes').then((m) => m.routes), + import('./modules/product/product.routes').then(m => m.routes) }, + { + path: 'auth/github/callback', + component: GithubCallbackComponent + } ]; diff --git a/marketplace-ui/src/app/auth/auth.service.spec.ts b/marketplace-ui/src/app/auth/auth.service.spec.ts new file mode 100644 index 000000000..c55faf239 --- /dev/null +++ b/marketplace-ui/src/app/auth/auth.service.spec.ts @@ -0,0 +1,133 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { Router } from '@angular/router'; +import { CookieService } from 'ngx-cookie-service'; +import { AuthService } from './auth.service'; +import { environment } from '../../environments/environment'; +import { of } from 'rxjs'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('AuthService', () => { + let service: AuthService; + let httpMock: HttpTestingController; + let routerSpy: jasmine.SpyObj; + let cookieServiceSpy: jasmine.SpyObj; + + beforeEach(() => { + const routerSpyObj = jasmine.createSpyObj('Router', ['navigate']); + const cookieSpyObj = jasmine.createSpyObj('CookieService', ['set', 'get']); + + TestBed.configureTestingModule({ + providers: [ + AuthService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: Router, useValue: routerSpyObj }, + { provide: CookieService, useValue: cookieSpyObj } + ] + }); + + service = TestBed.inject(AuthService); + httpMock = TestBed.inject(HttpTestingController); + routerSpy = TestBed.inject(Router) as jasmine.SpyObj; + cookieServiceSpy = TestBed.inject(CookieService) as jasmine.SpyObj; + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should exchange code for token', () => { + const code = 'testCode'; + const state = 'testState'; + const mockResponse = { token: 'testToken' }; + + spyOn(service, 'handleTokenResponse').and.callThrough(); + + service.handleGitHubCallback(code, state); + + const req = httpMock.expectOne(`${service['BASE_URL']}/auth/github/login`); + expect(req.request.method).toBe('POST'); + req.flush(mockResponse); + + expect(service['handleTokenResponse']).toHaveBeenCalledWith(mockResponse.token, state); + }); + + it('should handle error during token exchange', () => { + const code = 'testCode'; + const state = 'testState'; + service.handleGitHubCallback(code, state); + + const req = httpMock.expectOne(`${service['BASE_URL']}/auth/github/login`); + expect(req.request.method).toBe('POST'); + req.error(new ErrorEvent('Network error')); + }); + + it('should set token as cookie and navigate', () => { + const token = 'testToken'; + const state = 'testState'; + + service['handleTokenResponse'](token, state); + + expect(cookieServiceSpy.set).toHaveBeenCalledWith( + service['TOKEN_KEY'], + token, + {expires: jasmine.any(Number), path: '/'} + ); + expect(routerSpy.navigate).toHaveBeenCalledWith([state], { + queryParams: { showPopup: 'true' } + }); + }); + + it('should return null if token is expired', () => { + const token = 'expiredToken'; + spyOn(service as any, 'isTokenExpired').and.returnValue(true); + cookieServiceSpy.get.and.returnValue(token); + + const result = service.getToken(); + + expect(result).toBeNull(); + }); + + it('should return token if not expired', () => { + const token = 'validToken'; + spyOn(service as any, 'isTokenExpired').and.returnValue(false); + cookieServiceSpy.get.and.returnValue(token); + + const result = service.getToken(); + + expect(result).toBe(token); + }); + + it('should return display name from decoded token', () => { + const token = 'validToken'; + const decodedToken = { name: 'testName' }; + spyOn(service as any, 'decodeToken').and.returnValue(decodedToken); + spyOn(service, 'getToken').and.returnValue(token); + + const result = service.getDisplayName(); + + expect(result).toBe(decodedToken.name); + }); + + it('should return user ID from decoded token', () => { + const token = 'validToken'; + const decodedToken = { sub: 'testUserId' }; + spyOn(service as any, 'decodeToken').and.returnValue(decodedToken); + spyOn(service, 'getToken').and.returnValue(token); + + const result = service.getUserId(); + + expect(result).toBe(decodedToken.sub); + }); + + it('should extract number of expired days correctly', () => { + const token = 'validToken'; + const decodedToken = { exp: Math.floor(Date.now() / 1000) + 86400 }; + spyOn(service as any, 'decodeToken').and.returnValue(decodedToken); + + const result = service['extractNumberOfExpiredDay'](token); + + expect(result).toBe(1); + }); +}); diff --git a/marketplace-ui/src/app/auth/auth.service.ts b/marketplace-ui/src/app/auth/auth.service.ts new file mode 100644 index 000000000..39e4fccbb --- /dev/null +++ b/marketplace-ui/src/app/auth/auth.service.ts @@ -0,0 +1,140 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { Router } from '@angular/router'; +import { catchError, Observable, throwError } from 'rxjs'; +import { CookieService } from 'ngx-cookie-service'; +import { jwtDecode } from 'jwt-decode'; + +export interface TokenPayload { + username: string; + name: string; + sub: string; + exp: number; +} + +export interface RequestBody { + [key: string]: string; +} + +export interface TokenResponse { + token: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private readonly BASE_URL = environment.apiUrl; + private readonly TOKEN_KEY = 'token'; + private readonly githubAuthUrl = 'https://github.com/login/oauth/authorize'; + private readonly githubAuthCallbackUrl = environment.githubAuthCallbackUrl; + + constructor( + private readonly http: HttpClient, + private readonly router: Router, + private readonly cookieService: CookieService + ) {} + + redirectToGitHub(originalUrl: string): void { + const state = encodeURIComponent(originalUrl); + const authUrl = `${this.githubAuthUrl}?client_id=${environment.githubClientId}&redirect_uri=${this.githubAuthCallbackUrl}&state=${state}`; + window.location.href = authUrl; + } + + handleGitHubCallback(code: string, state: string): void { + const body = { code }; + + this.exchangeCodeForToken(body).subscribe({ + next: response => this.handleTokenResponse(response.token, state), + error: error => throwError(() => error) + }); + } + + private exchangeCodeForToken(body: RequestBody): Observable { + const url = `${this.BASE_URL}/auth/github/login`; + return this.http + .post(url, body) + .pipe(catchError(error => throwError(() => error))); + } + + handleTokenResponse(token: string, state: string): void { + this.setTokenAsCookie(token); + this.router.navigate([`${state}`], { + queryParams: { showPopup: 'true' } + }); + } + + private setTokenAsCookie(token: string): void { + this.cookieService.set(this.TOKEN_KEY, token, { + expires: this.extractNumberOfExpiredDay(token), + path: '/' + }); + } + + getToken(): string | null { + const token = this.cookieService.get(this.TOKEN_KEY); + if (token && !this.isTokenExpired(token)) { + return token; + } + return null; + } + + private decodeToken(token: string): TokenPayload | null { + try { + return jwtDecode(token); + } catch (error) { + return null; + } + } + + private isTokenExpired(token: string): boolean { + try { + const decoded = this.decodeToken(token); + if (decoded) { + if (!decoded?.exp) { + return false; + } + const currentTime = Math.floor(Date.now() / 1000); + return decoded.exp < currentTime; + } + } catch (error) { + return true; + } + return false; + } + + getDisplayName(): string | null { + const token = this.getToken(); + if (token && this.decodeToken(token)) { + const decoded = this.decodeToken(token); + if (decoded) { + return decoded.name || decoded.username; + } + return decoded; + } + return null; + } + + getUserId(): string | null { + const token = this.getToken(); + if (token) { + const decoded = this.decodeToken(token); + if (decoded) { + return decoded.sub; + } + return null; + } + return null; + } + + private extractNumberOfExpiredDay(token: string): number { + const exp = this.decodeToken(token)?.exp ?? 0; + + const expDate = new Date(exp * 1000); + const currentDate = new Date(); + + const diffTime = Math.abs(expDate.getTime() - currentDate.getTime()); + return Math.ceil(diffTime / environment.dayInMiliseconds); + } +} diff --git a/marketplace-ui/src/app/auth/github-callback/github-callback.component.spec.ts b/marketplace-ui/src/app/auth/github-callback/github-callback.component.spec.ts new file mode 100644 index 000000000..f50cf737a --- /dev/null +++ b/marketplace-ui/src/app/auth/github-callback/github-callback.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { GithubCallbackComponent } from './github-callback.component'; +import { ActivatedRoute } from '@angular/router'; +import { AuthService } from '../auth.service'; +import { of } from 'rxjs'; + +describe('GithubCallbackComponent', () => { + let component: GithubCallbackComponent; + let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let activatedRouteStub: Partial; + + beforeEach(async () => { + mockAuthService = jasmine.createSpyObj('AuthService', ['handleGitHubCallback']); + activatedRouteStub = { + queryParams: of({ code: 'testCode', state: 'testState' }) + }; + + await TestBed.configureTestingModule({ + providers: [ + { provide: AuthService, useValue: mockAuthService }, + { provide: ActivatedRoute, useValue: activatedRouteStub } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GithubCallbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call handleGitHubCallback with correct parameters', () => { + expect(mockAuthService.handleGitHubCallback).toHaveBeenCalledWith('testCode', 'testState'); + }); + + it('should not call handleGitHubCallback if code or state is missing', () => { + activatedRouteStub.queryParams = of({ code: 'testCode' }); // Missing state + fixture = TestBed.createComponent(GithubCallbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(mockAuthService.handleGitHubCallback).not.toHaveBeenCalledWith('testCode', undefined!); + + activatedRouteStub.queryParams = of({ state: 'testState' }); // Missing code + fixture = TestBed.createComponent(GithubCallbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(mockAuthService.handleGitHubCallback).not.toHaveBeenCalledWith(undefined!, 'testState'); + }); +}); diff --git a/marketplace-ui/src/app/auth/github-callback/github-callback.component.ts b/marketplace-ui/src/app/auth/github-callback/github-callback.component.ts new file mode 100644 index 000000000..85b80ffd4 --- /dev/null +++ b/marketplace-ui/src/app/auth/github-callback/github-callback.component.ts @@ -0,0 +1,24 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { AuthService } from '../auth.service'; + +@Component({ + selector: 'app-github-callback', + standalone: true, + imports: [], + template: '' +}) +export class GithubCallbackComponent implements OnInit { + route = inject(ActivatedRoute); + authService = inject(AuthService); + + ngOnInit(): void { + this.route.queryParams.subscribe(params => { + const code = params['code']; + const state = params['state']; + if (code && state) { + this.authService.handleGitHubCallback(code, state); + } + }); + } +} diff --git a/marketplace-ui/src/app/core/configs/markdown.config.ts b/marketplace-ui/src/app/core/configs/markdown.config.ts new file mode 100644 index 000000000..bf18cb185 --- /dev/null +++ b/marketplace-ui/src/app/core/configs/markdown.config.ts @@ -0,0 +1,16 @@ +import { MarkedOptions, MarkedRenderer } from 'ngx-markdown'; + +export function markedOptionsFactory(): MarkedOptions { + const renderer = new MarkedRenderer(); + + renderer.blockquote = (text: string) => { + return '

' + text + '

'; + }; + + return { + renderer: renderer, + gfm: true, + breaks: false, + pedantic: false + }; +} diff --git a/marketplace-ui/src/app/core/configs/translate.config.ts b/marketplace-ui/src/app/core/configs/translate.config.ts index 9c6ad62c0..d3cb52e88 100644 --- a/marketplace-ui/src/app/core/configs/translate.config.ts +++ b/marketplace-ui/src/app/core/configs/translate.config.ts @@ -1,6 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { TranslateLoader } from '@ngx-translate/core'; -import { Observable, map } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { parse } from 'yaml'; class TranslateYamlHttpLoader implements TranslateLoader { diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts index 936e55e82..249a97ec0 100644 --- a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -1,11 +1,15 @@ -import { HttpHeaders, HttpContextToken, HttpInterceptorFn } from '@angular/common/http'; +import { + HttpHeaders, + HttpContextToken, + HttpInterceptorFn +} from '@angular/common/http'; import { environment } from '../../../environments/environment'; import { LoadingService } from '../services/loading/loading.service'; import { inject } from '@angular/core'; import { finalize } from 'rxjs'; -export const REQUEST_BY = "X-Requested-By"; -export const IVY = "ivy"; +export const REQUEST_BY = 'X-Requested-By'; +export const IVY = 'ivy'; /** This is option for exclude loading api * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(SkipLoading, true) }) @@ -23,8 +27,12 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { if (!requestURL.startsWith(apiURL)) { requestURL = `${apiURL}/${req.url}`; } - const cloneReq = req.clone({ url: requestURL, headers: addIvyHeaders(req.headers) }); - + + const cloneReq = req.clone({ + url: requestURL, + headers: addIvyHeaders(req.headers) + }); + if (req.context.get(SkipLoading)) { return next(cloneReq); } diff --git a/marketplace-ui/src/app/core/services/language/language.service.spec.ts b/marketplace-ui/src/app/core/services/language/language.service.spec.ts index 14a2b853e..6e741f9ea 100644 --- a/marketplace-ui/src/app/core/services/language/language.service.spec.ts +++ b/marketplace-ui/src/app/core/services/language/language.service.spec.ts @@ -20,11 +20,11 @@ describe('LanguageService', () => { it('should get default language en', () => { document.defaultView?.localStorage.clear(); - expect(service.getSelectedLanguage()).toEqual(Language.EN); + expect(service.selectedLanguage()).toEqual(Language.EN); }); it('should change to language de-DE', ()=> { - service.loadLanguage("de"); - expect(service.getSelectedLanguage()).toEqual(Language.DE); + service.loadLanguage(Language.DE); + expect(service.selectedLanguage()).toEqual(Language.DE); }); }); diff --git a/marketplace-ui/src/app/core/services/language/language.service.ts b/marketplace-ui/src/app/core/services/language/language.service.ts index 85a2732a3..8d4ce476c 100644 --- a/marketplace-ui/src/app/core/services/language/language.service.ts +++ b/marketplace-ui/src/app/core/services/language/language.service.ts @@ -1,11 +1,14 @@ import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable } from '@angular/core'; +import { computed, Inject, Injectable, signal } from '@angular/core'; import { Language } from '../../../shared/enums/language.enum'; const DATA_LANGUAGE = 'data-language'; @Injectable({ providedIn: 'root' }) export class LanguageService { + private readonly language = signal(Language.EN); + selectedLanguage = computed(() => this.language() ?? Language.EN) + constructor(@Inject(DOCUMENT) private readonly document: Document) { const localStorage = this.document.defaultView?.localStorage; if (localStorage) { @@ -14,15 +17,20 @@ export class LanguageService { } loadDefaultLanguage(localStorage: Storage) { - const language = localStorage.getItem(DATA_LANGUAGE); - this.loadLanguage(language ?? Language.EN); + const language = localStorage.getItem(DATA_LANGUAGE) as Language; + if (this.isValidLanguage(language)) { + this.loadLanguage(language); + } else { + this.loadLanguage(Language.EN); + } } - loadLanguage(language: string): void { - localStorage.setItem(DATA_LANGUAGE, language); + private isValidLanguage(language: Language) { + return Object.values(Language).includes(language); } - getSelectedLanguage(): Language { - return localStorage.getItem(DATA_LANGUAGE) as Language ?? Language.EN; + loadLanguage(language: Language): void { + localStorage.setItem(DATA_LANGUAGE, language); + this.language.set(language); } } diff --git a/marketplace-ui/src/app/core/services/loading/loading.service.ts b/marketplace-ui/src/app/core/services/loading/loading.service.ts index 0536aa06c..f3981781c 100644 --- a/marketplace-ui/src/app/core/services/loading/loading.service.ts +++ b/marketplace-ui/src/app/core/services/loading/loading.service.ts @@ -1,4 +1,4 @@ -import { Injectable, computed, signal } from '@angular/core'; +import { computed, Injectable, signal } from '@angular/core'; @Injectable({ providedIn: 'root' diff --git a/marketplace-ui/src/app/core/services/theme/theme.service.ts b/marketplace-ui/src/app/core/services/theme/theme.service.ts index acc332e75..e956901ec 100644 --- a/marketplace-ui/src/app/core/services/theme/theme.service.ts +++ b/marketplace-ui/src/app/core/services/theme/theme.service.ts @@ -1,5 +1,5 @@ import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable, WritableSignal, signal } from '@angular/core'; +import { Inject, Injectable, signal, WritableSignal } from '@angular/core'; import { Theme } from '../../../shared/enums/theme.enum'; const DATA_THEME = 'data-bs-theme'; diff --git a/marketplace-ui/src/app/modules/home/home.component.spec.ts b/marketplace-ui/src/app/modules/home/home.component.spec.ts index 453fa72b3..efa53a1b8 100644 --- a/marketplace-ui/src/app/modules/home/home.component.spec.ts +++ b/marketplace-ui/src/app/modules/home/home.component.spec.ts @@ -30,4 +30,4 @@ describe('HomeComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); -}); +}); \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html index dc91c8667..e5da67672 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html @@ -5,7 +5,7 @@ width="70" height="70" [ngSrc]="product | logo" - [alt]="product.names | multilingualism: languageService.getSelectedLanguage()" /> + [alt]="product.names | multilingualism: languageService.selectedLanguage()" />
@@ -15,13 +15,13 @@
{{ - product.names | multilingualism: languageService.getSelectedLanguage() + product.names | multilingualism: languageService.selectedLanguage() }}

{{ product.shortDescriptions - | multilingualism: languageService.getSelectedLanguage() + | multilingualism: languageService.selectedLanguage() }}

diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts index 79fe77af4..bcd05bc71 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts @@ -1,9 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { - MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS, - MOCK_PRODUCTS -} from '../../../shared/mocks/mock-data'; +import { MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS, MOCK_PRODUCTS } from '../../../shared/mocks/mock-data'; import { ProductCardComponent } from './product-card.component'; import { Product } from '../../../shared/models/product.model'; import { Language } from '../../../shared/enums/language.enum'; diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts index 307436298..f8e404cb1 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts @@ -1,5 +1,5 @@ import { CommonModule, NgOptimizedImage } from '@angular/common'; -import { Component, Input, inject } from '@angular/core'; +import { Component, inject, Input } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { LanguageService } from '../../../core/services/language/language.service'; import { ThemeService } from '../../../core/services/theme/theme.service'; diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.html new file mode 100644 index 000000000..196c02984 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.html @@ -0,0 +1,15 @@ +
+ +
+ + + +@if (isShowBtnMore()) { +
+ +
+} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.scss new file mode 100644 index 000000000..840bf4131 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.scss @@ -0,0 +1,8 @@ +.btn-show-more { + margin-top: 48px; + border-radius: 10px; + padding: 12px 50px; + font-weight: 500; + font-size: 16px; + line-height: 120%; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts new file mode 100644 index 000000000..7c8dd9719 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts @@ -0,0 +1,134 @@ +import { + ComponentFixture, + TestBed, + fakeAsync, + tick +} from '@angular/core/testing'; +import { ProductDetailFeedbackComponent } from './product-detail-feedback.component'; +import { ProductStarRatingPanelComponent } from './product-star-rating-panel/product-star-rating-panel.component'; +import { ShowFeedbacksDialogComponent } from './show-feedbacks-dialog/show-feedbacks-dialog.component'; +import { ProductFeedbacksPanelComponent } from './product-feedbacks-panel/product-feedbacks-panel.component'; +import { AppModalService } from '../../../../shared/services/app-modal.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ProductFeedbackService } from './product-feedbacks-panel/product-feedback.service'; +import { AuthService } from '../../../../auth/auth.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { ProductStarRatingService } from './product-star-rating-panel/product-star-rating.service'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { StarRatingCounting } from '../../../../shared/models/star-rating-counting.model'; +import { signal } from '@angular/core'; +import { Feedback } from '../../../../shared/models/feedback.model'; + +describe('ProductDetailFeedbackComponent', () => { + let component: ProductDetailFeedbackComponent; + let fixture: ComponentFixture; + let mockAppModalService: jasmine.SpyObj; + let mockProductFeedbackService: jasmine.SpyObj; + let mockProductStarRatingService: jasmine.SpyObj; + let mockAuthService: jasmine.SpyObj; + let mockActivatedRoute: any; + let mockRouter: jasmine.SpyObj; + + beforeEach(async () => { + mockAppModalService = jasmine.createSpyObj('AppModalService', [ + 'openAddFeedbackDialog', + 'openShowFeedbacksDialog' + ]); + mockProductFeedbackService = jasmine.createSpyObj( + 'ProductFeedbackService', + [ + 'initFeedbacks', + 'findProductFeedbackOfUser', + 'loadMoreFeedbacks', + 'areAllFeedbacksLoaded', + 'totalElements' + ], + {feedbacks: signal([] as Feedback[]), sort: signal('updatedAt,desc')} + ); + mockProductStarRatingService = jasmine.createSpyObj( + 'ProductStarRatingService', + ['fetchData'], + { + reviewNumber: signal(0), + totalComments: signal(0), + starRatings: signal([] as StarRatingCounting[]) + } + ); + mockAuthService = jasmine.createSpyObj('AuthService', ['getToken']); + mockActivatedRoute = { queryParams: of({ showPopup: 'true' }) }; + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + + await TestBed.configureTestingModule({ + imports: [ProductDetailFeedbackComponent, TranslateModule.forRoot()], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: AppModalService, useValue: mockAppModalService }, + { + provide: ProductFeedbackService, + useValue: mockProductFeedbackService + }, + { + provide: ProductStarRatingService, + useValue: mockProductStarRatingService + }, + { provide: AuthService, useValue: mockAuthService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: Router, useValue: mockRouter } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductDetailFeedbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize feedbacks and star ratings on ngOnInit', () => { + expect( + mockProductFeedbackService.findProductFeedbackOfUser + ).toHaveBeenCalled(); + expect(mockProductStarRatingService.fetchData).toHaveBeenCalled(); + }); + + it('should render ProductStarRatingPanelComponent and ProductFeedbacksPanelComponent', () => { + const starRatingPanel = fixture.debugElement.query( + By.directive(ProductStarRatingPanelComponent) + ); + const feedbacksPanel = fixture.debugElement.query( + By.directive(ProductFeedbacksPanelComponent) + ); + + expect(starRatingPanel).toBeTruthy(); + expect(feedbacksPanel).toBeTruthy(); + }); + + it('should call openShowFeedbacksDialog on button click if not in mobile mode', () => { + spyOn(component, 'isMobileMode').and.returnValue(false); + const button = fixture.debugElement.query( + By.css('.btn-show-more') + ).nativeElement; + button.click(); + expect(mockAppModalService.openShowFeedbacksDialog).toHaveBeenCalled(); + }); + + it('should call loadMoreFeedbacks on button click if in mobile mode', () => { + spyOn(component, 'isMobileMode').and.returnValue(true); + const button = fixture.debugElement.query( + By.css('.btn-show-more') + ).nativeElement; + button.click(); + expect(mockProductFeedbackService.loadMoreFeedbacks).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.ts new file mode 100644 index 000000000..ebc5c5872 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.ts @@ -0,0 +1,87 @@ +import { + AfterViewInit, + Component, + computed, + inject, + input, + OnInit, + Signal +} from '@angular/core'; +import { ProductStarRatingPanelComponent } from './product-star-rating-panel/product-star-rating-panel.component'; +import { ShowFeedbacksDialogComponent } from './show-feedbacks-dialog/show-feedbacks-dialog.component'; +import { ProductFeedbacksPanelComponent } from './product-feedbacks-panel/product-feedbacks-panel.component'; +import { AppModalService } from '../../../../shared/services/app-modal.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ProductFeedbackService } from './product-feedbacks-panel/product-feedback.service'; +import { AuthService } from '../../../../auth/auth.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { ProductStarRatingService } from './product-star-rating-panel/product-star-rating.service'; + +const MAX_ELEMENTS = 6; + +@Component({ + selector: 'app-product-detail-feedback', + standalone: true, + imports: [ + ProductStarRatingPanelComponent, + ShowFeedbacksDialogComponent, + ProductFeedbacksPanelComponent, + TranslateModule + ], + templateUrl: './product-detail-feedback.component.html', + styleUrls: ['./product-detail-feedback.component.scss'] +}) +export class ProductDetailFeedbackComponent implements OnInit, AfterViewInit { + isMobileMode = input(); + isShowBtnMore: Signal = computed(() => { + if ( + this.productFeedbackService.areAllFeedbacksLoaded() && + (this.isMobileMode() || + this.productFeedbackService.totalElements() <= MAX_ELEMENTS) + ) { + return false; + } + return true; + }); + + productFeedbackService = inject(ProductFeedbackService); + appModalService = inject(AppModalService); + private readonly productStarRatingService = inject(ProductStarRatingService); + private readonly authService = inject(AuthService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + + showPopup!: boolean; + + ngOnInit(): void { + this.productFeedbackService.findProductFeedbackOfUser(); + this.productStarRatingService.fetchData(); + } + + ngAfterViewInit(): void { + this.route.queryParams.subscribe(params => { + this.showPopup = params['showPopup'] === 'true'; + if (this.showPopup && this.authService.getToken()) { + this.appModalService.openAddFeedbackDialog().then( + () => this.removeQueryParam(), + () => this.removeQueryParam() + ); + } + }); + } + + openShowFeedbacksDialog(): void { + if (this.isMobileMode()) { + this.productFeedbackService.loadMoreFeedbacks(); + } else { + this.appModalService.openShowFeedbacksDialog(); + } + } + + private removeQueryParam(): void { + this.router.navigate([], { + queryParams: { showPopup: null }, + queryParamsHandling: 'merge' + }); + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html new file mode 100644 index 000000000..1741cde6d --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html @@ -0,0 +1,14 @@ +
+

{{'common.sort.label' | translate}}:

+ +
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss new file mode 100644 index 000000000..25842222f --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss @@ -0,0 +1,20 @@ +select { + border-radius: 10px; + font-size: 14px; + line-height: 120%; + font-weight: 400; + padding: 10px 15px 10px auto; + height: 37px; + opacity: 0.8; + border: 1px solid; + cursor: pointer; + option { + padding: 10px; + font-weight: 400; + font-size: 14px; + } +} + +p { + margin-right: 20px; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts new file mode 100644 index 000000000..8ff05bdb8 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts @@ -0,0 +1,65 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { FeedbackFilterComponent } from './feedback-filter.component'; +import { ProductFeedbackService } from '../product-feedback.service'; +import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.constant'; + +describe('FeedbackFilterComponent', () => { + let component: FeedbackFilterComponent; + let fixture: ComponentFixture; + let translateService: jasmine.SpyObj; + let productFeedbackService: jasmine.SpyObj; + + beforeEach(async () => { + const productFeedbackServiceSpy = jasmine.createSpyObj('ProductFeedbackService', ['sort']); + + await TestBed.configureTestingModule({ + imports: [FeedbackFilterComponent, FormsModule, TranslateModule.forRoot() ], + providers: [ + TranslateService, + { provide: ProductFeedbackService, useValue: productFeedbackServiceSpy } + ] + }) + .compileComponents(); + + translateService = TestBed.inject(TranslateService) as jasmine.SpyObj; + productFeedbackService = TestBed.inject(ProductFeedbackService) as jasmine.SpyObj; + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FeedbackFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render sort options from feedbackSortTypes', () => { + const selectElement: HTMLSelectElement = fixture.nativeElement.querySelector('select'); + const options = selectElement ? selectElement.querySelectorAll('option') : []; + + expect(options.length).toBe(FEEDBACK_SORT_TYPES.length); + + FEEDBACK_SORT_TYPES.forEach((type, index) => { + const option = options[index] as HTMLOptionElement | null; + expect(option).withContext('Option element should exist'); + if (option) { + expect(option.textContent?.trim()).toBe(type.label); + expect(option.value).toBe(type.sortFn); + } + }); + }); + + it('should emit sortChange event on select change', () => { + const selectElement: HTMLSelectElement = fixture.nativeElement.querySelector('select'); + const emitSpy = spyOn(component.sortChange, 'emit').and.callThrough(); + + selectElement.value = 'updatedAt,asc'; // Simulate select change + selectElement.dispatchEvent(new Event('change')); + + expect(emitSpy).toHaveBeenCalledWith('updatedAt,asc'); + }); +}); \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts new file mode 100644 index 000000000..e8b316b5a --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, inject, Output } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.constant'; +import { FormsModule } from '@angular/forms'; +import { ProductFeedbackService } from '../product-feedback.service'; + +@Component({ + selector: 'app-feedback-filter', + standalone: true, + imports: [FormsModule, TranslateModule], + templateUrl: './feedback-filter.component.html', + styleUrl: './feedback-filter.component.scss' +}) +export class FeedbackFilterComponent { + feedbackSortTypes = FEEDBACK_SORT_TYPES; + + @Output() sortChange = new EventEmitter(); + + productFeedbackService = inject(ProductFeedbackService); + + onSortChange(event: Event): void { + const selectElement = event.target as HTMLSelectElement; + this.sortChange.emit(selectElement.value); + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts new file mode 100644 index 000000000..2c0103dd4 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts @@ -0,0 +1,133 @@ +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { AuthService } from '../../../../../auth/auth.service'; +import { ProductDetailService } from '../../product-detail.service'; +import { ProductStarRatingService } from '../product-star-rating-panel/product-star-rating.service'; +import { ProductFeedbackService } from './product-feedback.service'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { Feedback } from '../../../../../shared/models/feedback.model'; + +describe('ProductFeedbackService', () => { + let service: ProductFeedbackService; + let httpMock: HttpTestingController; + let authService: jasmine.SpyObj; + let productDetailService: jasmine.SpyObj; + let productStarRatingService: jasmine.SpyObj; + + beforeEach(() => { + const authServiceSpy = jasmine.createSpyObj('AuthService', ['getToken', 'getUserId']); + const productDetailServiceSpy = jasmine.createSpyObj('ProductDetailService', ['productId']); + const productStarRatingServiceSpy = jasmine.createSpyObj('ProductStarRatingService', ['fetchData']); + + TestBed.configureTestingModule({ + providers: [ + ProductFeedbackService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: AuthService, useValue: authServiceSpy }, + { provide: ProductDetailService, useValue: productDetailServiceSpy }, + { provide: ProductStarRatingService, useValue: productStarRatingServiceSpy } + ] + }); + + service = TestBed.inject(ProductFeedbackService); + httpMock = TestBed.inject(HttpTestingController); + authService = TestBed.inject(AuthService) as jasmine.SpyObj; + productDetailService = TestBed.inject(ProductDetailService) as jasmine.SpyObj; + productStarRatingService = TestBed.inject(ProductStarRatingService) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should submit feedback successfully', () => { + const feedback: Feedback = { + content: 'Great product!', + rating: 5, + productId: '123' + }; + authService.getToken.and.returnValue('mockToken'); + + service.submitFeedback(feedback).subscribe(result => { + expect(result).toEqual(feedback); + }); + + const req = httpMock.expectOne('api/feedback'); + expect(req.request.method).toBe('POST'); + expect(req.request.headers.get('Authorization')).toBe('Bearer mockToken'); + req.flush(feedback); + expect(productStarRatingService.fetchData).toHaveBeenCalled(); + }); + + it('should initialize feedbacks', () => { + const mockResponse = { + _embedded: { feedbacks: [{ content: 'Great product!', rating: 5, productId: '123' }] }, + page: { totalPages: 2, totalElements: 5 } + }; + + productDetailService.productId.and.returnValue('123'); + + service.initFeedbacks(); + const req = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=updatedAt,desc'); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + + expect(service.totalPages()).toBe(2); + expect(service.totalElements()).toBe(5); + expect(service.feedbacks()).toEqual([{ content: 'Great product!', rating: 5, productId: '123' }]); + }); + + it('should load more feedbacks', () => { + const initialFeedback: Feedback[] = [ + { content: 'Great product!', rating: 5, productId: '123' } + ]; + const additionalFeedback: Feedback[] = [ + { content: 'Another review', rating: 4, productId: '123' } + ]; + + productDetailService.productId.and.returnValue('123'); + service.initFeedbacks(); + const initReq = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=updatedAt,desc'); + initReq.flush({ _embedded: { feedbacks: initialFeedback }, page: { totalPages: 2, totalElements: 5 } }); + + service.loadMoreFeedbacks(); + const loadMoreReq = httpMock.expectOne('api/feedback/product/123?page=1&size=8&sort=updatedAt,desc'); + loadMoreReq.flush({ _embedded: { feedbacks: additionalFeedback } }); + + expect(service.feedbacks()).toEqual([...initialFeedback, ...additionalFeedback]); + }); + + it('should change sort and fetch feedbacks', () => { + const mockResponse = { + _embedded: { feedbacks: [{ content: 'Sorting test', rating: 3, productId: '123' }] } + }; + + productDetailService.productId.and.returnValue('123'); + + service.changeSort('rating,desc'); + const req = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=rating,desc'); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + + expect(service.feedbacks()).toEqual([{ content: 'Sorting test', rating: 3, productId: '123' }]); + }); + + it('should find product feedback of user', () => { + const mockFeedback: Feedback = { + content: 'User feedback', + rating: 5, + productId: '123' + }; + + authService.getUserId.and.returnValue('user123'); + productDetailService.productId.and.returnValue('123'); + + service.findProductFeedbackOfUser(); + const req = httpMock.expectOne('api/feedback?productId=123&userId=user123'); + expect(req.request.method).toBe('GET'); + req.flush(mockFeedback); + + expect(service.userFeedback()).toEqual(mockFeedback); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts new file mode 100644 index 000000000..22da90117 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts @@ -0,0 +1,145 @@ +import { + HttpClient, + HttpContext, + HttpHeaders, + HttpParams +} from '@angular/common/http'; +import { + computed, + inject, + Injectable, + signal, + WritableSignal +} from '@angular/core'; +import { catchError, Observable, of, tap } from 'rxjs'; +import { AuthService } from '../../../../../auth/auth.service'; +import { SkipLoading } from '../../../../../core/interceptors/api.interceptor'; +import { FeedbackApiResponse } from '../../../../../shared/models/apis/feedback-response.model'; +import { Feedback } from '../../../../../shared/models/feedback.model'; +import { ProductDetailService } from '../../product-detail.service'; +import { ProductStarRatingService } from '../product-star-rating-panel/product-star-rating.service'; + +const FEEDBACK_API_URL = 'api/feedback'; +const SIZE = 8; +@Injectable({ + providedIn: 'root' +}) +export class ProductFeedbackService { + private readonly authService = inject(AuthService); + private readonly productDetailService = inject(ProductDetailService); + private readonly productStarRatingService = inject(ProductStarRatingService); + private readonly http = inject(HttpClient); + + sort: WritableSignal = signal('updatedAt,desc'); + page: WritableSignal = signal(0); + + userFeedback: WritableSignal = signal(null); + feedbacks: WritableSignal = signal([]); + areAllFeedbacksLoaded = computed(() => { + if (this.page() >= this.totalPages() - 1) { + return true; + } + return false; + }); + + totalPages: WritableSignal = signal(1); + totalElements: WritableSignal = signal(0); + + submitFeedback(feedback: Feedback): Observable { + const headers = new HttpHeaders().set( + 'Authorization', + `Bearer ${this.authService.getToken()}` + ); + return this.http + .post(FEEDBACK_API_URL, feedback, { + headers, + context: new HttpContext().set(SkipLoading, true) + }) + .pipe( + tap(() => { + this.initFeedbacks(); + this.findProductFeedbackOfUser(); + this.productStarRatingService.fetchData(); + }) + ); + } + + private findProductFeedbacksByCriteria( + productId: string = this.productDetailService.productId(), + page: number = this.page(), + sort: string = this.sort(), + size: number = SIZE + ): Observable { + const requestParams = new HttpParams() + .set('page', page.toString()) + .set('size', size.toString()) + .set('sort', sort); + const requestURL = `${FEEDBACK_API_URL}/product/${productId}`; + return this.http + .get(requestURL, { + params: requestParams, + context: new HttpContext().set(SkipLoading, true) + }) + .pipe( + tap(response => { + if (page === 0) { + this.feedbacks.set(response._embedded.feedbacks); + } else { + this.feedbacks.set([ + ...this.feedbacks(), + ...response._embedded.feedbacks + ]); + } + }) + ); + } + + findProductFeedbackOfUser( + productId: string = this.productDetailService.productId() + ): void { + const params = new HttpParams() + .set('productId', productId) + .set('userId', this.authService.getUserId() ?? ''); + const requestURL = FEEDBACK_API_URL; + + this.http + .get(requestURL, { + params, + context: new HttpContext().set(SkipLoading, true) + }) + .pipe( + tap(feedback => { + this.userFeedback.set(feedback); + }), + catchError(() => { + const feedback: Feedback = { + content: '', + rating: 0, + productId + }; + this.userFeedback.set(feedback); + return of(feedback); + }) + ) + .subscribe(); + } + + initFeedbacks(): void { + this.page.set(0); + this.findProductFeedbacksByCriteria().subscribe(response => { + this.totalPages.set(response.page.totalPages); + this.totalElements.set(response.page.totalElements); + }); + } + + loadMoreFeedbacks(): void { + this.page.update(value => value + 1); + this.findProductFeedbacksByCriteria().subscribe(); + } + + changeSort(newSort: string): void { + this.page.set(0); + this.sort.set(newSort); + this.findProductFeedbacksByCriteria().subscribe(); + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.html new file mode 100644 index 000000000..bc78215df --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.html @@ -0,0 +1,34 @@ +
+ +
+ + + + {{ isExpanded() ? 'See less' : 'See more' }} + + +
+
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.scss new file mode 100644 index 000000000..ba539acca --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.scss @@ -0,0 +1,89 @@ +@use 'sass:math'; + +// Variables for commonly used values +$font-size: 12.91px; +$icon-size: 18.56px; +$gap-large: 13px; +$gap-small: 10px; +$height-small: 19px; +$height-large: 40px; +$aspect-ratio: math.div(1, 1); + +.d-flex { + display: flex; +} + +.flex-column { + flex-direction: column; +} + +.flex-row { + flex-direction: row; +} + +.align-items-center { + align-items: center; +} + +.justify-content-between { + justify-content: space-between; +} + +.p-0 { + padding: 0; +} + +.h-100 { + height: 100%; +} + +.rounded-circle { + border-radius: 50%; +} + +.img-fit-cover { + object-fit: cover; + aspect-ratio: $aspect-ratio; +} + +.img-avatar { + height: 100%; +} + +.icon-feedback { + font-size: $icon-size; +} + +.feedback-header { + height: $height-large; + gap: $gap-large; +} + +.feedback-username { + gap: $gap-small; +} + +.star-rating-container { + height: $height-small; + font-size: $font-size; +} + +.collapsed { + display: -webkit-box; + -webkit-line-clamp: 6; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.expanded { + display: block; +} + +.expand-toggle-link { + cursor: pointer; + color: var(--ivy-active-color); + font-size: 14px; + font-weight: 400; + line-height: 21px; +} + diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.spec.ts new file mode 100644 index 000000000..bffc76b70 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProductFeedbackComponent } from './product-feedback.component'; +import { CommonModule } from '@angular/common'; +import { StarRatingComponent } from '../../../../../../shared/components/star-rating/star-rating.component'; +import { ElementRef } from '@angular/core'; +import { Feedback } from '../../../../../../shared/models/feedback.model'; + +describe('ProductFeedbackComponent', () => { + let component: ProductFeedbackComponent; + let fixture: ComponentFixture; + let mockElementRef: ElementRef; + + beforeEach(async () => { + mockElementRef = { + nativeElement: { + scrollHeight: 200, + clientHeight: 100 + } as HTMLElement + } as ElementRef; + + await TestBed.configureTestingModule({ + imports: [ProductFeedbackComponent, StarRatingComponent, CommonModule], + providers: [ + { provide: ElementRef, useValue: mockElementRef } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductFeedbackComponent); + component = fixture.componentInstance; + component.feedback = { + username: 'Test User', + userAvatarUrl: 'avatar-url', + rating: 4, + content: 'This is a test feedback content.' + } as Feedback; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle content visibility when toggleContent is called', () => { + component.toggleContent(); + expect(component.isExpanded()).toBe(true); + + component.toggleContent(); + expect(component.isExpanded()).toBe(false); + }); +}); + diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.ts new file mode 100644 index 000000000..f894643c9 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback/product-feedback.component.ts @@ -0,0 +1,36 @@ +import { Component, ElementRef, HostListener, Input, signal, ViewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { StarRatingComponent } from '../../../../../../shared/components/star-rating/star-rating.component'; +import { Feedback } from '../../../../../../shared/models/feedback.model'; + +@Component({ + selector: 'app-product-feedback', + standalone: true, + imports: [CommonModule, StarRatingComponent], + templateUrl: './product-feedback.component.html', + styleUrl: './product-feedback.component.scss' +}) +export class ProductFeedbackComponent { + @Input() feedback!: Feedback; + @ViewChild('content') contentElement!: ElementRef; + + showToggle = signal(false); + isExpanded = signal(false); + + ngAfterViewInit() { + this.setShowToggle(); + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.setShowToggle(); + } + + private setShowToggle() { + this.showToggle.set(this.contentElement.nativeElement.scrollHeight > this.contentElement.nativeElement.clientHeight); + } + + toggleContent() { + this.isExpanded.set(!this.isExpanded()); + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.html new file mode 100644 index 000000000..37067feed --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.html @@ -0,0 +1,27 @@ +
+ + @if (isRenderInModalDialog) { +
+ @for (feedback of feedbacks(); track $index) { + + } +
+ } @else if (isMobileMode()) { +
+ @for (feedback of feedbacks(); track $index) { + + } +
+ } @else { +
+ @for (feedback of feedbacks() | slice:0:6; track $index) { + + } +
+ } +
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.scss new file mode 100644 index 000000000..579ec15f8 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.scss @@ -0,0 +1,70 @@ +// Variables for commonly used values +$gap-large: 48px; +$gap-small: 34px; +$height-header: 37px; +$height-dialog: 733px; + +.d-flex { + display: flex; +} + +.flex-column { + flex-direction: column; +} + +.flex-row { + flex-direction: row; +} + +.p-0 { + padding: 0; +} + +.align-items-center { + align-items: center; +} + +.justify-content-between { + justify-content: space-between; +} + +.h-100 { + height: 100%; +} + +.text-secondary { + color: var(--bs-secondary); +} + +.overflow-auto { + overflow: auto; +} + +.grid-feedbacks-container { + display: grid; + gap: $gap-large; // Adjust the gap between items as needed + + // Large screens + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); + } + + // Small screens + @media (max-width: 767px) { + grid-template-columns: 1fr; + } +} + +.grid-feedbacks-container-dialog { + display: grid; + gap: $gap-small; + grid-template-columns: 1fr; + padding-bottom: $gap-small; + height: $height-dialog; + scrollbar-width: none; +} + +.feedback-header { + height: $height-header; + gap: $gap-large; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.spec.ts new file mode 100644 index 000000000..878d78b65 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProductFeedbacksPanelComponent } from './product-feedbacks-panel.component'; +import { FeedbackFilterComponent } from './feedback-filter/feedback-filter.component'; +import { CommonModule } from '@angular/common'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ThemeService } from '../../../../../core/services/theme/theme.service'; +import { ProductFeedbackService } from './product-feedback.service'; +import { ProductDetailService } from '../../product-detail.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('ProductFeedbacksPanelComponent', () => { + let component: ProductFeedbacksPanelComponent; + let fixture: ComponentFixture; + let productFeedbackService: ProductFeedbackService; + let translateService: TranslateService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ ProductFeedbacksPanelComponent, FeedbackFilterComponent, CommonModule, TranslateModule.forRoot() ], + providers: [ + ProductFeedbackService, + ProductDetailService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ThemeService, + TranslateService + ], + schemas: [ NO_ERRORS_SCHEMA ] + }).compileComponents(); + + fixture = TestBed.createComponent(ProductFeedbacksPanelComponent); + component = fixture.componentInstance; + productFeedbackService = TestBed.inject(ProductFeedbackService); + + component.isRenderInModalDialog = false; + fixture.componentRef.setInput( + 'isMobileMode', + false + ); + spyOn(productFeedbackService, 'changeSort').and.callThrough(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should trigger sorting when sortChange event is emitted', () => { + const sortType = 'createdAt,desc'; + component.onSortChange(sortType); + expect(productFeedbackService.changeSort).toHaveBeenCalledWith(sortType); + }); + + it('should load more feedbacks on scroll check if not all loaded', () => { + const mockEvent = { + target: { + scrollTop: 200, + offsetHeight: 200, + scrollHeight: 400 + } + } as any; + + spyOn(productFeedbackService, 'areAllFeedbacksLoaded').and.returnValue(false); + spyOn(productFeedbackService, 'loadMoreFeedbacks').and.callThrough(); + + component.onScrollCheckAllFeedbacksLoaded(mockEvent); + + expect(productFeedbackService.loadMoreFeedbacks).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.ts new file mode 100644 index 000000000..6f2c46cca --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedbacks-panel.component.ts @@ -0,0 +1,66 @@ +import { + Component, + EventEmitter, + inject, + Input, + input, + Output, + Signal +} from '@angular/core'; +import { ProductFeedbackComponent } from './product-feedback/product-feedback.component'; +import { ProductFeedbackService } from './product-feedback.service'; +import { FeedbackFilterComponent } from './feedback-filter/feedback-filter.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ThemeService } from '../../../../../core/services/theme/theme.service'; +import { Feedback } from '../../../../../shared/models/feedback.model'; +import { CommonModule } from '@angular/common'; +import { ProductDetailService } from '../../product-detail.service'; + +interface CustomElement extends HTMLElement { + scrollTop: number; + offsetHeight: number; + scrollHeight: number; +} + +@Component({ + selector: 'app-product-feedbacks-panel', + standalone: true, + imports: [ + CommonModule, + ProductFeedbackComponent, + FeedbackFilterComponent, + TranslateModule + ], + templateUrl: './product-feedbacks-panel.component.html', + styleUrl: './product-feedbacks-panel.component.scss' +}) +export class ProductFeedbacksPanelComponent { + isMobileMode = input(); + + @Input() isRenderInModalDialog = false; + @Output() showFeedbacksLoadedBtn = new EventEmitter(); + + themeService = inject(ThemeService); + productFeedbackService = inject(ProductFeedbackService); + productDetailService = inject(ProductDetailService); + + feedbacks: Signal = + this.productFeedbackService.feedbacks; + + onSortChange(sort: string): void { + this.productFeedbackService.changeSort(sort); + } + + onScrollCheckAllFeedbacksLoaded(e: Event): void { + const element = e.target as CustomElement; + const threshold = 50; + const position = element.scrollTop + element.offsetHeight; + const height = element.scrollHeight; + if ( + position >= height - threshold && + !this.productFeedbackService.areAllFeedbacksLoaded() + ) { + this.productFeedbackService.loadMoreFeedbacks(); + } + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html new file mode 100644 index 000000000..1a6c585a6 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html @@ -0,0 +1,51 @@ + diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss new file mode 100644 index 000000000..fd7be1452 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss @@ -0,0 +1,95 @@ +i { + position: relative; + display: inline-block; + font-size: 2.5rem; + color: #dfdfdf; +} + +.filled { + color: #FFCB13; + overflow: hidden; + position: absolute; + top: 0; + left: 0; +} + +.add-feedback-title { + font-weight: 600; +} + +.product-item-select { + border-radius: 10px; + font-size: 14px; + line-height: 120%; + font-weight: 400; + padding: 10px 35px 10px 15px; + height: 37px; + cursor: pointer; + opacity: 0.8; + border: 1px solid; +} + +.feedback-content-textarea { + flex: auto; + padding: 10px; + margin-top: 10px; + width: 100%; + resize: none; + background: var(--ivy-textarea-background-color); + border: 0.5px solid var(--footer-border-color); + border-radius: 5px; + font-weight: 400; + font-size: 14px; + line-height: 120%; + scrollbar-width: none; +} + +.submit-feedback-btn { + width: 100%; + padding: 12px 32px; + border-radius: 10px; + font-weight: 500; + font-size: 16px; + line-height: 120%; +} + +.user-link { + text-align: center; + text-decoration-line: underline; + color: var(--ivy-active-color); + cursor: pointer; +} + +.modal-body-wrapper { + display: flex; + flex-direction: column; + margin: auto; +} + +@media (max-width: 767px) { + .modal-body-wrapper { + padding: 20px; + padding-top: 60px; + gap: 30px; + max-width: 400px; + min-width: 350px; + width: 100%; + } + + .product-item-select { + width: 100%; + } +} + +@media (min-width: 768px) { + .modal-body-wrapper { + padding-bottom: 24px; + gap: 20px; + width: 525px; + } + + .product-item-select { + width: fit-content; + } +} + diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.spec.ts new file mode 100644 index 000000000..0a2e8d220 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.spec.ts @@ -0,0 +1,125 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { AuthService } from '../../../../../../auth/auth.service'; +import { ProductDetailService } from '../../../product-detail.service'; +import { ProductFeedbackService } from '../../product-feedbacks-panel/product-feedback.service'; +import { AddFeedbackDialogComponent } from './add-feedback-dialog.component'; +import { Feedback } from '../../../../../../shared/models/feedback.model'; +import { signal } from '@angular/core'; +import { of } from 'rxjs'; + +describe('AddFeedbackDialogComponent', () => { + let component: AddFeedbackDialogComponent; + let fixture: ComponentFixture; + let authServiceMock: jasmine.SpyObj; + let productFeedbackServiceMock: jasmine.SpyObj; + let productDetailServiceMock: jasmine.SpyObj; + let activeModalMock: jasmine.SpyObj; + + beforeEach(async () => { + const authServiceSpy = jasmine.createSpyObj('AuthService', [ + 'getDisplayName' + ]); + const productFeedbackServiceSpy = jasmine.createSpyObj( + 'ProductFeedbackService', + ['submitFeedback'], + { userFeedback: signal({}) } + ); + const productDetailServiceSpy = jasmine.createSpyObj( + 'ProductDetailService', + ['productId'], + { + productNames: signal({ en: 'en', de: 'de' }), + productId: signal('mockProductId') + } + ); + const activeModalSpy = jasmine.createSpyObj('NgbActiveModal', [ + 'close', + 'dismiss' + ]); + + await TestBed.configureTestingModule({ + imports: [ + AddFeedbackDialogComponent, + FormsModule, + TranslateModule.forRoot() + ], + providers: [ + { provide: AuthService, useValue: authServiceSpy }, + { + provide: ProductFeedbackService, + useValue: productFeedbackServiceSpy + }, + { provide: ProductDetailService, useValue: productDetailServiceSpy }, + { provide: NgbActiveModal, useValue: activeModalSpy } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(AddFeedbackDialogComponent); + component = fixture.componentInstance; + authServiceMock = TestBed.inject( + AuthService + ) as jasmine.SpyObj; + productFeedbackServiceMock = TestBed.inject( + ProductFeedbackService + ) as jasmine.SpyObj; + productDetailServiceMock = TestBed.inject( + ProductDetailService + ) as jasmine.SpyObj; + activeModalMock = TestBed.inject( + NgbActiveModal + ) as jasmine.SpyObj; + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddFeedbackDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize displayName with AuthService data on ngOnInit', () => { + const mockDisplayName = 'John Doe'; + authServiceMock.getDisplayName.and.returnValue(mockDisplayName); + + component.ngOnInit(); + + expect(component.displayName).toBe(mockDisplayName); + }); + + it('should initialize feedback object on ngOnInit', () => { + const mockFeedback: Feedback = { + content: 'Test feedback content', + rating: 4, + productId: 'mockProductId' + }; + + productFeedbackServiceMock.userFeedback.set(mockFeedback); + + component.ngOnInit(); + + expect(component.feedback).toEqual(mockFeedback); + }); + + it('should submit feedback and close modal on onSubmitFeedback', () => { + const mockFeedback: Feedback = { + content: 'Test feedback content', + rating: 4, + productId: 'mockProductId' + }; + + component.feedback = mockFeedback; + + productFeedbackServiceMock.submitFeedback.and.returnValue(of(mockFeedback)); + + component.onSubmitFeedback(); + + expect(productFeedbackServiceMock.submitFeedback).toHaveBeenCalledWith(mockFeedback); + expect(activeModalMock.close).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts new file mode 100644 index 000000000..0532a5270 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts @@ -0,0 +1,68 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject, Signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { AuthService } from '../../../../../../auth/auth.service'; +import { LanguageService } from '../../../../../../core/services/language/language.service'; +import { StarRatingComponent } from '../../../../../../shared/components/star-rating/star-rating.component'; +import { Feedback } from '../../../../../../shared/models/feedback.model'; +import { MultilingualismPipe } from '../../../../../../shared/pipes/multilingualism.pipe'; +import { AppModalService } from '../../../../../../shared/services/app-modal.service'; +import { ProductDetailService } from '../../../product-detail.service'; +import { ProductFeedbackService } from '../../product-feedbacks-panel/product-feedback.service'; +import { throwError } from 'rxjs'; + +@Component({ + selector: 'app-add-feedback-dialog', + standalone: true, + templateUrl: './add-feedback-dialog.component.html', + styleUrl: './add-feedback-dialog.component.scss', + imports: [ + CommonModule, + StarRatingComponent, + FormsModule, + TranslateModule, + MultilingualismPipe + ] +}) +export class AddFeedbackDialogComponent { + productFeedbackService = inject(ProductFeedbackService); + productDetailService = inject(ProductDetailService); + activeModal = inject(NgbActiveModal); + languageService = inject(LanguageService); + private readonly authService = inject(AuthService); + private readonly appModalService = inject(AppModalService); + + displayName = ''; + feedback!: Feedback; + + userFeedback: Signal = + this.productFeedbackService.userFeedback; + + ngOnInit() { + const displayName = this.authService.getDisplayName(); + if (displayName) { + this.displayName = displayName; + } + this.feedback = { + content: this.userFeedback()?.content ?? '', + rating: this.userFeedback()?.rating ?? 0, + productId: this.productDetailService.productId() + }; + } + + onSubmitFeedback(): void { + this.productFeedbackService.submitFeedback(this.feedback).subscribe({ + complete: () => { + this.activeModal.close(); + this.appModalService.openSuccessDialog(); + }, + error: error => throwError(() => error) + }); + } + + onRateChange(newRate: number) { + this.feedback.rating = newRate; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.html new file mode 100644 index 000000000..6cc10d0a8 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.html @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.scss new file mode 100644 index 000000000..aa5849123 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.scss @@ -0,0 +1,9 @@ +.modal-body { + align-content: center; + text-align: center; + padding-bottom: 120px; + + img { + margin-bottom: 20px; + } +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.spec.ts new file mode 100644 index 000000000..ad37b8d65 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SuccessDialogComponent } from './success-dialog.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../../../../../../auth/auth.service'; +import { NgOptimizedImage } from '@angular/common'; + +describe('SuccessDialogComponent', () => { + let component: SuccessDialogComponent; + let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + + beforeEach(async () => { + const authServiceSpy = jasmine.createSpyObj('AuthService', ['getDisplayName']); + + await TestBed.configureTestingModule({ + imports: [SuccessDialogComponent, TranslateModule.forRoot(), NgOptimizedImage], + providers: [ + NgbActiveModal, + { provide: AuthService, useValue: authServiceSpy } + ] + }).compileComponents(); + + mockAuthService = TestBed.inject(AuthService) as jasmine.SpyObj; + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SuccessDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display translated thank message with displayName', () => { + const mockDisplayName = 'John Doe'; + mockAuthService.getDisplayName.and.returnValue(mockDisplayName); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + const thankMessageElement = compiled.querySelector('.modal-body h4.text-primary:last-child'); + expect(thankMessageElement).toBeTruthy(); + expect(thankMessageElement.textContent.trim()).toContain(`common.feedback.thankMessage ${mockDisplayName}`); + }); + + it('should dismiss modal when close button is clicked', () => { + const dismissSpy = spyOn(component.activeModal, 'dismiss'); + const closeButton = fixture.nativeElement.querySelector('.btn-close'); + closeButton.click(); + expect(dismissSpy).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts new file mode 100644 index 000000000..9790866c9 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { AuthService } from '../../../../../../../auth/auth.service'; +import { NgOptimizedImage } from '@angular/common'; + +@Component({ + selector: 'app-success-dialog', + standalone: true, + imports: [TranslateModule, NgOptimizedImage], + templateUrl: './success-dialog.component.html', + styleUrls: ['./success-dialog.component.scss'] +}) +export class SuccessDialogComponent { + + activeModal = inject(NgbActiveModal); + + authService = inject(AuthService); +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html new file mode 100644 index 000000000..eb1c26356 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html @@ -0,0 +1,29 @@ +
+ +
+
+

+ {{ 'common.feedback.detailedReviews' | translate }} +

+
+ @for (counting of starRatings(); track $index) { +
+ + +
+ } +
+
+
\ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.scss new file mode 100644 index 000000000..c1f6654a6 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.scss @@ -0,0 +1,66 @@ +.start-rating-counting-line { + display: grid; + grid-template-columns: max-content auto; + align-items: center; + + .star-index { + padding-right: 4px; + display: inline-flex; + + .number-star-rating { + padding: 0 1px; + font-family: 'Inter'; + font-size: 18px; + font-weight: 400; + line-height: 25.2px; + text-align: left; + } + + .mini-star-icon { + padding: 0 2.5px; + display: flex; + font-size: 8px; + align-content: center; + flex-wrap: wrap; + + .star-rating-count { + width: 11px; + display: flex; + height: 11px; + align-items: center; + justify-content: space-evenly; + } + } + } + + .star-rating-counting-line { + width: 100%; + border-radius: 100px; + height: 8px; + + .star-rating-percent { + border-radius: 100px; + height: 100%; + } + } +} + +.grid-product-star-rating-panel-container { + display: grid; +} + +.grid-50-50 { + grid-template-columns: 45% 10% 45%; +} + +.grid-100 { + grid-template-columns: 1fr; + gap: 20px; + margin-top: 0.5rem; +} + +@media (min-width: 768px) { + .grid-50-50 { + grid-template-columns: 25% 5% 70%; + } +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.spec.ts new file mode 100644 index 000000000..2fdd0341e --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.spec.ts @@ -0,0 +1,103 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { ProductStarRatingPanelComponent } from './product-star-rating-panel.component'; +import { ProductStarRatingService } from './product-star-rating.service'; +import { ProductStarRatingNumberComponent } from '../../product-star-rating-number/product-star-rating-number.component'; +import { StarRatingHighlightDirective } from './star-rating-highlight.directive'; +import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { signal } from '@angular/core'; + +describe('ProductStarRatingPanelComponent', () => { + let component: ProductStarRatingPanelComponent; + let fixture: ComponentFixture; + let productStarRatingServiceMock: jasmine.SpyObj; + + beforeEach(async () => { + const productStarRatingServiceSpy = jasmine.createSpyObj( + 'ProductStarRatingService', + [], + { + reviewNumber: signal(0), + totalComments: signal(0), + starRatings: signal([] as StarRatingCounting[]) + } + ); + + await TestBed.configureTestingModule({ + imports: [ + ProductStarRatingPanelComponent, + CommonModule, + TranslateModule.forRoot(), + ProductStarRatingNumberComponent, + StarRatingHighlightDirective + ], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { + provide: ProductStarRatingService, + useValue: productStarRatingServiceSpy + } + ] + }).compileComponents(); + + productStarRatingServiceMock = TestBed.inject( + ProductStarRatingService + ) as jasmine.SpyObj; + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductStarRatingPanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit openAddFeedbackDialog event when child component triggers the event', () => { + spyOn(component.openAddFeedbackDialog, 'emit'); + const starRatingNumberComponent = + fixture.debugElement.nativeElement.querySelector( + 'app-product-star-rating-number' + ); + starRatingNumberComponent.dispatchEvent(new Event('openAddFeedbackDialog')); + + expect(component.openAddFeedbackDialog.emit).toHaveBeenCalled(); + }); + + it('should render star ratings correctly', () => { + const mockStarRatings: StarRatingCounting[] = [ + { starRating: 5, percent: 80 }, + { starRating: 4, percent: 15 }, + { starRating: 3, percent: 5 } + ]; + productStarRatingServiceMock.starRatings.set(mockStarRatings); + + fixture.detectChanges(); + + const starRatingElements = fixture.nativeElement.querySelectorAll( + '.start-rating-counting-line' + ); + expect(starRatingElements.length).toBe(mockStarRatings.length); + + mockStarRatings.forEach((rating, index) => { + const starRatingElement = starRatingElements[index]; + expect( + starRatingElement + .querySelector('.number-star-rating') + .textContent.trim() + ).toBe(rating.starRating.toString()); + expect( + starRatingElement.querySelector('.star-rating-percent').style.width + ).toBe(`${rating.percent}%`); + }); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts new file mode 100644 index 000000000..86888b5e2 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, inject, Input, input, Output, Signal } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ProductStarRatingService } from './product-star-rating.service'; + +import { StarRatingHighlightDirective } from './star-rating-highlight.directive'; +import { + ProductStarRatingNumberComponent +} from '../../product-star-rating-number/product-star-rating-number.component'; +import { CommonModule } from '@angular/common'; +import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model'; + +@Component({ + selector: 'app-product-star-rating-panel', + standalone: true, + imports: [ + CommonModule, + ProductStarRatingNumberComponent, + StarRatingHighlightDirective, + TranslateModule + ], + templateUrl: './product-star-rating-panel.component.html', + styleUrl: './product-star-rating-panel.component.scss' +}) +export class ProductStarRatingPanelComponent { + isMobileMode = input(); + + @Input() isRenderInModalDialog = false; + @Output() openAddFeedbackDialog = new EventEmitter(); + + productStarRatingService = inject(ProductStarRatingService); + + starRatings: Signal = this.productStarRatingService.starRatings; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.spec.ts new file mode 100644 index 000000000..01dee76b9 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.spec.ts @@ -0,0 +1,81 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { ProductStarRatingService } from './product-star-rating.service'; +import { ProductDetailService } from '../../product-detail.service'; +import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('ProductStarRatingService', () => { + let service: ProductStarRatingService; + let httpMock: HttpTestingController; + let productDetailService: jasmine.SpyObj; + + const mockStarRatings: StarRatingCounting[] = [ + { starRating: 5, commentNumber: 10 }, + { starRating: 4, commentNumber: 5 }, + { starRating: 3, commentNumber: 2 } + ]; + + beforeEach(() => { + const productDetailServiceSpy = jasmine.createSpyObj('ProductDetailService', ['productId']); + + TestBed.configureTestingModule({ + providers: [ + ProductStarRatingService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: ProductDetailService, useValue: productDetailServiceSpy } + ] + }); + + service = TestBed.inject(ProductStarRatingService); + httpMock = TestBed.inject(HttpTestingController); + productDetailService = TestBed.inject(ProductDetailService) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should fetch data and set star ratings', () => { + const productId = '123'; + productDetailService.productId.and.returnValue(productId); + + service.fetchData(); + + const req = httpMock.expectOne(`api/feedback/product/${productId}/rating`); + expect(req.request.method).toBe('GET'); + req.flush(mockStarRatings); + + expect(service.starRatings()).toEqual([ + { starRating: 5, commentNumber: 10 }, + { starRating: 4, commentNumber: 5 }, + { starRating: 3, commentNumber: 2 } + ]); + }); + + it('should calculate total comments', () => { + service.starRatings.set(mockStarRatings); + expect(service.totalComments()).toBe(17); + }); + + it('should calculate review number', () => { + service.starRatings.set(mockStarRatings); + expect(service.reviewNumber()).toBe(4.5); + }); + + it('should sort star ratings by star rating', () => { + const unsortedRatings = [ + { starRating: 3, commentNumber: 2 }, + { starRating: 5, commentNumber: 10 }, + { starRating: 4, commentNumber: 5 } + ]; + + service['sortByStar'](unsortedRatings); + expect(unsortedRatings).toEqual([ + { starRating: 5, commentNumber: 10 }, + { starRating: 4, commentNumber: 5 }, + { starRating: 3, commentNumber: 2 } + ]); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts new file mode 100644 index 000000000..9f4345c61 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { + computed, + inject, + Injectable, + Signal, + signal, + WritableSignal +} from '@angular/core'; +import { tap } from 'rxjs'; +import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model'; +import { ProductDetailService } from '../../product-detail.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductStarRatingService { + private readonly productDetailService = inject(ProductDetailService); + private readonly http = inject(HttpClient); + + starRatings: WritableSignal = signal([]); + totalComments: Signal = computed(() => + this.calculateTotalComments(this.starRatings()) + ); + reviewNumber: Signal = computed(() => + this.calculateReviewNumber(this.starRatings()) + ); + + fetchData(productId: string = this.productDetailService.productId()): void { + const requestURL = `api/feedback/product/${productId}/rating`; + this.http + .get(requestURL) + .pipe( + tap(data => { + this.sortByStar(data); + this.starRatings.set(data); + }) + ) + .subscribe(); + } + + private sortByStar(starRatings: StarRatingCounting[]): void { + starRatings.sort((a, b) => b.starRating - a.starRating); + } + + private calculateTotalComments(starRatings: StarRatingCounting[]): number { + let totalComments = 0; + starRatings.forEach(starRating => { + totalComments += starRating.commentNumber ?? 0; + }); + return totalComments; + } + + private calculateReviewNumber(starRatings: StarRatingCounting[]): number { + let reviewNumber = 0; + const totalComments = this.calculateTotalComments(starRatings); + starRatings.forEach(starRating => { + reviewNumber += starRating.starRating * (starRating.commentNumber ?? 1); + }); + if (totalComments > 0) { + reviewNumber = reviewNumber / this.calculateTotalComments(starRatings); + } + + return Math.round(reviewNumber * 10) / 10; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.spec.ts new file mode 100644 index 000000000..ee88f5e29 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.spec.ts @@ -0,0 +1,52 @@ +import { StarRatingHighlightDirective } from './star-rating-highlight.directive'; +import { Component, ElementRef } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; + +@Component({ + template: `
` +}) +class TestComponent { + percent = 50; +} + +describe('StarRatingHighlightDirective', () => { + let fixture: ComponentFixture; + let component: TestComponent; + let el: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [StarRatingHighlightDirective], + declarations: [TestComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + el = fixture.nativeElement.querySelector('div'); + }); + + it('should create an instance', () => { + const directive = new StarRatingHighlightDirective(new ElementRef(el)); + expect(directive).toBeTruthy(); + }); + + it('should set the width based on percent input', () => { + component.percent = 75; + fixture.detectChanges(); + expect(el.style.width).toBe('75%'); + + component.percent = 25; + fixture.detectChanges(); + expect(el.style.width).toBe('25%'); + }); + + it('should update the width when percent input changes', () => { + component.percent = 50; + fixture.detectChanges(); + expect(el.style.width).toBe('50%'); + + component.percent = 100; + fixture.detectChanges(); + expect(el.style.width).toBe('100%'); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.ts new file mode 100644 index 000000000..7a3df97fd --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/star-rating-highlight.directive.ts @@ -0,0 +1,20 @@ +import { Directive, ElementRef, Input } from '@angular/core'; + +@Directive({ + selector: '[starRatingHighlight]', + standalone: true +}) +export class StarRatingHighlightDirective { + + @Input() percent!: number; + + constructor(private readonly el: ElementRef) { } + + ngOnChanges() { + this.width(this.percent); + } + + private width(percent: number) { + this.el.nativeElement.style.width = percent + "%"; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.html new file mode 100644 index 000000000..b95d33308 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.html @@ -0,0 +1,20 @@ + diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.scss new file mode 100644 index 000000000..b5807fc78 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.scss @@ -0,0 +1,48 @@ +// Variables for commonly used values +$modal-margin: 40px; +$star-rating-width: 218px; +$separator-width: 60px; +$feedback-panel-width: 525px; +$separator-height: 817px; +$separator-top: -15px; +$separator-border: 1px solid #ebebeb; + +.modal-content-wrapper { + .modal-header { + border-bottom: 0; + } + + .modal-body { + margin-left: $modal-margin; + margin-right: $modal-margin; + } + + .star-rating { + width: $star-rating-width; + } + + .separator { + width: $separator-width; + display: flex; + justify-content: center; + + .vr { + position: relative; + height: $separator-height; + top: $separator-top; + border: $separator-border; + } + } + + .feedback-panel { + width: $feedback-panel-width; + } +} + +.d-flex { + display: flex; +} + +.justify-content-center { + justify-content: center; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts new file mode 100644 index 000000000..e0ff75b7c --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts @@ -0,0 +1,77 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ShowFeedbacksDialogComponent } from './show-feedbacks-dialog.component'; +import { ProductFeedbacksPanelComponent } from '../product-feedbacks-panel/product-feedbacks-panel.component'; +import { ProductStarRatingPanelComponent } from '../product-star-rating-panel/product-star-rating-panel.component'; +import { AppModalService } from '../../../../../shared/services/app-modal.service'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('ShowFeedbacksDialogComponent', () => { + let component: ShowFeedbacksDialogComponent; + let fixture: ComponentFixture; + let mockActiveModal: jasmine.SpyObj; + let mockAppModalService: jasmine.SpyObj; + + beforeEach(async () => { + mockActiveModal = jasmine.createSpyObj('NgbActiveModal', ['dismiss']); + mockAppModalService = jasmine.createSpyObj('AppModalService', [ + 'openAddFeedbackDialog' + ]); + + await TestBed.configureTestingModule({ + imports: [ + ShowFeedbacksDialogComponent, + CommonModule, + ProductFeedbacksPanelComponent, + ProductStarRatingPanelComponent, + TranslateModule.forRoot() + ], + providers: [ + TranslateService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: NgbActiveModal, useValue: mockActiveModal }, + { provide: AppModalService, useValue: mockAppModalService } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ShowFeedbacksDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render ProductStarRatingPanelComponent and ProductFeedbacksPanelComponent', () => { + const starRatingPanel = fixture.debugElement.query( + By.directive(ProductStarRatingPanelComponent) + ); + const feedbacksPanel = fixture.debugElement.query( + By.directive(ProductFeedbacksPanelComponent) + ); + + expect(starRatingPanel).toBeTruthy(); + expect(feedbacksPanel).toBeTruthy(); + }); + + it('should call openAddFeedbackDialog on ProductStarRatingPanelComponent event', () => { + const starRatingPanel = fixture.debugElement.query( + By.directive(ProductStarRatingPanelComponent) + ); + starRatingPanel.triggerEventHandler('openAddFeedbackDialog', null); + fixture.detectChanges(); + + expect(mockAppModalService.openAddFeedbackDialog).toHaveBeenCalled(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.ts new file mode 100644 index 000000000..3f041d7d6 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.ts @@ -0,0 +1,25 @@ +import { Component, HostListener, inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ProductFeedbacksPanelComponent } from '../product-feedbacks-panel/product-feedbacks-panel.component'; +import { ProductStarRatingPanelComponent } from '../product-star-rating-panel/product-star-rating-panel.component'; +import { AppModalService } from '../../../../../shared/services/app-modal.service'; + +@Component({ + selector: 'app-show-feedbacks-dialog', + standalone: true, + imports: [ProductFeedbacksPanelComponent, ProductStarRatingPanelComponent], + templateUrl: './show-feedbacks-dialog.component.html', + styleUrl: './show-feedbacks-dialog.component.scss' +}) +export class ShowFeedbacksDialogComponent { + activeModal = inject(NgbActiveModal); + appModalService = inject(AppModalService); + + @HostListener('window:resize', ['$event']) + onResize() { + const mediaQuery = window.matchMedia('(max-width: 767px)'); + if (mediaQuery.matches) { + this.activeModal.dismiss(); + } + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html new file mode 100644 index 000000000..caf883de5 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html @@ -0,0 +1,101 @@ +

+ {{ 'common.product.detail.information.label' | translate }} +

+
+
+ + {{ 'common.product.detail.information.value.author' | translate }} + + {{ productDetail.vendor }} +
+
+
+ + {{ 'common.product.detail.information.value.version' | translate }} + + + {{ selectedVersion.replaceAll('Version ', '') }} + +
+
+
+ + {{ 'common.product.detail.information.value.compatibility' | translate }} + + + {{ productDetail.compatibility }} + +
+
+
+ + {{ 'common.product.detail.information.value.cost' | translate }} + + {{ productDetail.cost }} +
+
+
+ + {{ 'common.product.detail.information.value.language' | translate }} + + {{ productDetail.language }} +
+
+
+ + {{ 'common.product.detail.type' | translate }} + + {{ productDetail.type }} +
+
+
+ + {{ 'common.product.detail.information.value.industry' | translate }} + + {{ productDetail.industry }} +
+
+
+ + {{ 'common.product.detail.information.value.tag' | translate }} + + + {{ productDetail.tags ? productDetail.tags!.join(', ') : '' }} + +
+
+
+ + {{ 'common.product.detail.information.value.source' | translate }} + + + + github.com + + +
+
+
+ + {{ 'common.product.detail.information.value.status' | translate }} + + +
+
+
+ + {{ + 'common.product.detail.information.value.moreInformation' | translate + }} + + + + {{ 'common.product.detail.information.value.contactUs' | translate }} + + +
+
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.scss new file mode 100644 index 000000000..157120245 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.scss @@ -0,0 +1,21 @@ +.info-title { + padding-top: 2px; +} + +.info-container { + gap: 0.5rem; + + .status__image { + height: 20.36px; + border-radius: 2px; + } + + span { + height: 22px; + } + + hr { + border-bottom: 1px solid #f8f8f8; + margin: 0; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts new file mode 100644 index 000000000..e34f3636d --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductDetailInformationTabComponent } from './product-detail-information-tab.component'; +import { MOCK_PRODUCT_DETAILS } from '../../../../shared/mocks/mock-data'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +describe('InformationDetailComponent', () => { + let component: ProductDetailInformationTabComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ProductDetailInformationTabComponent, + TranslateModule.forRoot() + ], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(ProductDetailInformationTabComponent); + component = fixture.componentInstance; + component.productDetail = MOCK_PRODUCT_DETAILS; + component.selectedVersion = '1.0.0'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts new file mode 100644 index 000000000..8c8ca8822 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ProductDetail } from '../../../../shared/models/product-detail.model'; + +@Component({ + selector: 'app-product-detail-information-tab', + standalone: true, + imports: [CommonModule, TranslateModule], + templateUrl: './product-detail-information-tab.component.html', + styleUrl: './product-detail-information-tab.component.scss' +}) +export class ProductDetailInformationTabComponent { + @Input() + productDetail!: ProductDetail; + @Input() + selectedVersion!: string; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html new file mode 100644 index 000000000..ef973f8cf --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html @@ -0,0 +1,20 @@ +

+ + <!-- {{ productModuleContent.name }} --> +
+ <dependency> +
+   <groupId>{{ productModuleContent.groupId }}</groupId> +
+   <artifactId>{{ + productModuleContent.artifactId + }}</artifactId> +
+   <version>{{ + selectedVersion.replaceAll('Version ', '') + }}</version> +
+   <type>{{ productModuleContent.type }}</type> +
+ </dependency> +
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts new file mode 100644 index 000000000..c2c5a897f --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProductDetailMavenContentComponent } from './product-detail-maven-content.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { MOCK_PRODUCT_DETAILS } from '../../../../shared/mocks/mock-data'; + +describe('ProductDetailMavenContentComponent', () => { + let component: ProductDetailMavenContentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductDetailMavenContentComponent, TranslateModule.forRoot()], + providers: [TranslateService] + }).compileComponents(); + + fixture = TestBed.createComponent(ProductDetailMavenContentComponent); + component = fixture.componentInstance; + component.productModuleContent = MOCK_PRODUCT_DETAILS.productModuleContent; + component.selectedVersion = '1.0.0'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts new file mode 100644 index 000000000..9d46efffb --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ProductModuleContent } from '../../../../shared/models/product-module-content.model'; + +@Component({ + selector: 'app-product-detail-maven-content', + standalone: true, + imports: [TranslateModule], + templateUrl: './product-detail-maven-content.component.html', + styleUrl: './product-detail-maven-content.component.scss' +}) +export class ProductDetailMavenContentComponent { + @Input() + productModuleContent!: ProductModuleContent; + @Input() + selectedVersion!: string; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html index dfbb4262d..a84532e69 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html @@ -1,85 +1,88 @@ -
- - - @if (isDropDownDisplayed()) { -
- diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss index e69de29bb..879d6799a 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss @@ -0,0 +1,262 @@ +.product-detail-container { + gap: 6rem; + margin-top: 4.5rem; +} + +.logo__image { + width: 70px; +} + +.logo__image-container { + margin-right: 20px; +} + +.link-to-main { + height: 43px; +} + +.back-link { + font-weight: 500; + line-height: 120%; +} + +.analysis-container { + gap: 10px; +} + +.product-analysis { + line-height: 110%; +} + +.analysis-title { + line-height: 140%; +} + +.version-container { + height: 79px; +} + +.detail-body { + gap: 4rem; +} + +.readme-content ::ng-deep { + h1 { + font-size: 24px; + } + + h2 { + font-size: 22px; + } + + h3 { + font-size: 20px; + } + + a { + word-break: break-all; + } + + img { + max-width: 100%; + } + + table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + overflow-x: auto; + display: block; + + th, + td { + text-align: left; + padding: 8px; + word-wrap: break-word; + white-space: normal; + font-size: 12px; + } + + tr { + border-bottom: 1px solid var(--ivy-border-color); + } + } +} + +.nav-tabs { + gap: 2rem; +} + +.nav-tabs a:hover { + cursor: pointer; +} + +.nav-item { + gap: 10px; + + a { + font-weight: 400; + font-size: 22px; + } + + a.active { + font-weight: 600; + } + + a.text-secondary.active { + color: #ffffff !important; + } +} + +.nav-tabs .nav-link.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 2px; + background-color: var(--active-tab-indicator-color); + z-index: 1; +} + +.tab-group { + gap: 40px; + + .tab-content { + .tab-pane { + opacity: 1; + transition: none; + } + } +} + +.form-select { + opacity: 80%; + padding: 10px 15px; + flex: 1 1 auto; + box-sizing: border-box; + border: 1px solid var(--active-tab-indicator-color); + border-radius: 10px; +} + +.dropdown-row, +.dropdown-container { + gap: 16px; +} + +.indicator-arrow__up { + --bs-form-select-bg-img: var(--ivy-custom-select-indicator); +} + +.dropdown-toggle::after { + display: none; +} + +.flexible-gap, +.module-gap { + gap: 4.5rem; +} + +.info-tab { + padding: 15px 0 15px 15px; + gap: 1rem; +} + +.info-icon { + font-size: 1.56rem; +} + +.info-dropdown { + display: none; + position: absolute; + top: 100%; + left: 0; + z-index: 999; + border-radius: 10px; + box-shadow: 0px 0px 15px var(--info-dropdown-border); + padding: 20px; + gap: 15px; + margin-top: 5px; + background-color: var(--info-dropdown-bg); +} + +.info-dropdown.show { + display: block; +} + +.product-title { + line-height: 79.2px; +} + +hr { + margin-top: 8rem; + margin-bottom: 8rem; + border: 1px solid var(--ivy-border-color); +} + +@media (max-width: 768px) { + .product-detail-container { + gap: 2rem; + margin-top: 31px; + } + + .logo__image { + width: 41px; + } + + .product-analysis { + font-size: 38px; + } + + .analysis-title { + font-size: 1rem; + } + + .version-container { + height: 43px; + } + + .flexible-gap, + .version-gap { + gap: 2rem; + } + + .module-gap { + gap: 3.15rem; + } + + .product-title { + font-size: 40px; + line-height: 44px; + } + + .analysis-container { + gap: 7px; + } +} + +@media (max-width: 430px) { + .product-title { + font-size: 36px; + } +} + +@media (max-width: 400px) { + .product-title { + font-size: 31px; + } +} + +.rate-empty-text { + color: var(--text-no-rating-color); + margin-top: 20px; + margin-bottom: 20px; + font-size: 16px; +} + +.rate-connector-btn { + padding: 12px 32px 12px 32px; + gap: 10px; + border-radius: 10px; + opacity: 0px; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts index 834b5b654..fbe7484ed 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts @@ -1,13 +1,25 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; -import { MOCK_PRODUCTS } from '../../../shared/mocks/mock-data'; +import { TranslateModule } from '@ngx-translate/core'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; +import { MarkdownModule } from 'ngx-markdown'; +import { of } from 'rxjs'; +import { TypeOption } from '../../../shared/enums/type-option.enum'; +import { + MOCK_PRODUCT_DETAILS, + MOCK_PRODUCT_MODULE_CONTENT, + MOCK_PRODUCTS +} from '../../../shared/mocks/mock-data'; import { MockProductService } from '../../../shared/mocks/mock-services'; import { ProductService } from '../product.service'; import { ProductDetailComponent } from './product-detail.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { Product } from '../../../shared/models/product.model'; +import { ProductModuleContent } from '../../../shared/models/product-module-content.model'; -const products = MOCK_PRODUCTS._embedded.products as Product[]; +const products = MOCK_PRODUCTS._embedded.products; +declare const viewport: Viewport; describe('ProductDetailComponent', () => { let component: ProductDetailComponent; @@ -15,14 +27,22 @@ describe('ProductDetailComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProductDetailComponent, TranslateModule.forRoot()], + imports: [ + ProductDetailComponent, + TranslateModule.forRoot(), + MarkdownModule.forRoot() + ], providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), { provide: ActivatedRoute, useValue: { snapshot: { - params: { id: products[0].id } - } + params: { id: products[0].id }, + queryParams: { type: TypeOption.CONNECTORS } + }, + fragment: of('description') } } ] @@ -43,6 +63,201 @@ describe('ProductDetailComponent', () => { }); it('should create', () => { - expect(component.product.names.en).toEqual(products[0].names.en); + expect(component.productDetail().names['en']).toEqual( + MOCK_PRODUCT_DETAILS.names['en'] + ); + }); + + it('should toggle isDropdownOpen on onShowDropdown', () => { + component.isDropdownOpen.set(false); + component.onShowInfoContent(); + expect(component.isDropdownOpen()).toBe(true); + + component.onShowInfoContent(); + expect(component.isTabDropdownShown()).toBe(false); + }); + + it('should reset state before fetching new product details', () => { + component.productDetail.set(MOCK_PRODUCT_DETAILS); + component.productModuleContent.set( + MOCK_PRODUCT_DETAILS.productModuleContent + ); + + expect(component.productDetail().id).toBe('jira-connector'); + expect(component.productModuleContent().name).toBe('Jira Connector'); + }); + + it('should update dropdown selection on updateDropdownSelection', () => { + document.body.innerHTML = ``; + component.activeTab = 'description'; + component.updateDropdownSelection(); + const dropdown = document.getElementById('nav_item') as HTMLSelectElement; + expect(dropdown.value).toBe('description'); + }); + + it('should call updateDropdownSelection when setActiveTab is called', () => { + spyOn(component, 'updateDropdownSelection'); + const tab = 'specifications'; + component.setActiveTab(tab); + expect(component.updateDropdownSelection).toHaveBeenCalled(); + }); + + it('should call setActiveTab and updateDropdownSelection on onTabChange', () => { + const event = { target: { value: 'description' } } as unknown as Event; + spyOn(component, 'setActiveTab'); + spyOn(component, 'updateDropdownSelection'); + + component.onTabChange(event); + + expect(component.setActiveTab).toHaveBeenCalledWith('description'); + }); + + it('should return true for description when it is not null and not empty', () => { + const mockContent: ProductModuleContent = { + ...MOCK_PRODUCT_MODULE_CONTENT, + description: {en: 'Test description'} + }; + + component.productModuleContent.set(mockContent); + expect(component.getContent('description')).toBeTrue(); + }); + + it('should return false for description when it is null or empty', () => { + const mockContentWithEmptyDescription: ProductModuleContent = + MOCK_PRODUCT_MODULE_CONTENT; + component.productModuleContent.set(mockContentWithEmptyDescription); + expect(component.getContent('description')).toBeFalse(); + + const mockContentWithNullDescription: ProductModuleContent = { + ...MOCK_PRODUCT_MODULE_CONTENT + }; + component.productModuleContent.set(mockContentWithNullDescription); + expect(component.getContent('description')).toBeFalse(); + }); + + it('should return true for setup when it is not null and not empty', () => { + const mockContent: ProductModuleContent = { + ...MOCK_PRODUCT_MODULE_CONTENT, + setup: 'Test setup' + }; + + component.productModuleContent.set(mockContent); + expect(component.getContent('setup')).toBeTrue(); + }); + + it('should return false for setup when it is null or empty', () => { + const mockContentWithEmptySetup: ProductModuleContent = + MOCK_PRODUCT_MODULE_CONTENT; + component.productModuleContent.set(mockContentWithEmptySetup); + expect(component.getContent('setup')).toBeFalse(); + + const mockContentWithNullSetup: ProductModuleContent = { + ...MOCK_PRODUCT_MODULE_CONTENT + }; + component.productModuleContent.set(mockContentWithNullSetup); + expect(component.getContent('setup')).toBeFalse(); + }); + + it('should display dropdown horizontally on small viewport', () => { + viewport.set(540); + const tabGroup = fixture.debugElement.query(By.css('.row-tab')); + tabGroup.triggerEventHandler('click', null); + + fixture.detectChanges(); + const dropdown = fixture.debugElement.query(By.css('.dropdown-tab')); + + expect(getComputedStyle(dropdown.nativeElement).flexDirection).toBe('row'); + }); + + it('should display dropdown instead of tabs when viewport width is 540px', () => { + const tabGroup = fixture.debugElement.query(By.css('.tab-group')); + const tabs = tabGroup.query(By.css('.row-tab d-none d-xl-block col-12')); + const dropdown = tabGroup.query(By.css('.dropdown-tab')); + + expect(tabs).toBeFalsy(); + expect(dropdown).toBeTruthy(); + }); + + it('should display tabs instead of dropdown when viewport width is above 540px', () => { + viewport.set(1920); + const tabGroup = fixture.debugElement.query(By.css('.tab-group')); + const dropdown = tabGroup.query( + By.css( + '.dropdown-tab d-block d-xl-none d-flex flex-row justify-content-center align-items-center w-100' + ) + ); + + expect(dropdown).toBeFalsy(); + }); + + it('should display info tab on click of info icon for smaller screens', () => { + viewport.set(540); + + let infoTab = fixture.debugElement.query( + By.css( + '.info-tab d-none d-xl-block d-flex flex-column flex-grow-1 align-items-start col-xl-3' + ) + ); + expect(infoTab).toBeFalsy(); + + const infoIcon = fixture.debugElement.query(By.css('.info-icon')); + infoIcon.triggerEventHandler('click', null); + fixture.detectChanges(); + + infoTab = fixture.debugElement.query(By.css('.info-tab')); + expect(infoTab).toBeTruthy(); + }); + + it('should call checkMediaSize on ngAfterViewInit', fakeAsync(() => { + spyOn(component, 'checkMediaSize'); + component.ngAfterViewInit(); + tick(); + expect(component.checkMediaSize).toHaveBeenCalled(); + })); + + it('should set isMobileMode based on window size', () => { + spyOn(window, 'matchMedia').and.returnValue({ + matches: true, + media: '', + addEventListener: () => {}, + removeEventListener: () => {}, + onchange: null, + addListener: function ( + callback: + | ((this: MediaQueryList, ev: MediaQueryListEvent) => any) + | null + ): void { + throw new Error('Function not implemented.'); + }, + removeListener: function ( + callback: + | ((this: MediaQueryList, ev: MediaQueryListEvent) => any) + | null + ): void { + throw new Error('Function not implemented.'); + }, + dispatchEvent: function (event: Event): boolean { + throw new Error('Function not implemented.'); + } + }); + + component.checkMediaSize(); + expect(component.isMobileMode()).toBeTrue(); + + (window.matchMedia as jasmine.Spy).and.returnValue({ + matches: false, + media: '', + addEventListener: () => {}, + removeEventListener: () => {} + }); + + component.checkMediaSize(); + expect(component.isMobileMode()).toBeFalse(); + }); + + it('should call checkMediaSize on window resize', () => { + spyOn(component, 'checkMediaSize'); + component.onResize(); + expect(component.checkMediaSize).toHaveBeenCalled(); }); }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts index 1d965d7a7..482a93e1d 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts @@ -1,33 +1,236 @@ -import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Product } from '../../../shared/models/product.model'; +import { + Component, + ElementRef, + HostListener, + WritableSignal, + inject, + signal +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; import { ProductService } from '../product.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { MarkdownModule, MarkdownService } from 'ngx-markdown'; +import { ProductDetail } from '../../../shared/models/product-detail.model'; +import { ProductModuleContent } from '../../../shared/models/product-module-content.model'; +import { ThemeService } from '../../../core/services/theme/theme.service'; +import { CommonModule } from '@angular/common'; +import { ProductDetailInformationTabComponent } from './product-detail-information-tab/product-detail-information-tab.component'; +import { ProductDetailVersionActionComponent } from './product-detail-version-action/product-detail-version-action.component'; +import { ProductDetailMavenContentComponent } from './product-detail-maven-content/product-detail-maven-content.component'; +import { PRODUCT_DETAIL_TABS } from '../../../shared/constants/common.constant'; +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import { LanguageService } from '../../../core/services/language/language.service'; import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe'; -import { ProductDetailVersionActionComponent } from './product-detail-version-action/product-detail-version-action.component'; +import { ProductDetailService } from './product-detail.service'; +import { ProductDetailFeedbackComponent } from './product-detail-feedback/product-detail-feedback.component'; +import { ProductFeedbackService } from './product-detail-feedback/product-feedbacks-panel/product-feedback.service'; +import { AppModalService } from '../../../shared/services/app-modal.service'; +import { AuthService } from '../../../auth/auth.service'; +import { ProductStarRatingNumberComponent } from './product-star-rating-number/product-star-rating-number.component'; +import { + ProductInstallationCountActionComponent +} from "./product-installation-count-action/product-installation-count-action.component"; +import { ProductTypeIconPipe } from '../../../shared/pipes/icon.pipe'; + +export interface DetailTab { + activeClass: string; + tabId: string; + value: string; + label: string; +} +const STORAGE_ITEM = 'activeTab'; +const DEFAULT_ACTIVE_TAB = 'description'; @Component({ selector: 'app-product-detail', standalone: true, - imports: [MultilingualismPipe, ProductDetailVersionActionComponent], - providers: [ProductService], + imports: [ + ProductDetailVersionActionComponent, + CommonModule, + ProductStarRatingNumberComponent, + TranslateModule, + MarkdownModule, + ProductDetailInformationTabComponent, + ProductDetailMavenContentComponent, + NgbNavModule, + MultilingualismPipe, + ProductDetailFeedbackComponent, + ProductInstallationCountActionComponent, + ProductTypeIconPipe, + ], + providers: [ProductService, MarkdownService], templateUrl: './product-detail.component.html', styleUrl: './product-detail.component.scss' }) export class ProductDetailComponent { - product!: Product; + themeService = inject(ThemeService); route = inject(ActivatedRoute); + router = inject(Router); productService = inject(ProductService); languageService = inject(LanguageService); - productId!: string; + productDetailService = inject(ProductDetailService); + productFeedbackService = inject(ProductFeedbackService); + appModalService = inject(AppModalService); + authService = inject(AuthService); + elementRef = inject(ElementRef); + + resizeObserver: ResizeObserver; + + productDetail: WritableSignal = signal({} as ProductDetail); + productModuleContent: WritableSignal = signal( + {} as ProductModuleContent + ); + detailContent!: DetailTab; + detailTabs = PRODUCT_DETAIL_TABS; + activeTab = DEFAULT_ACTIVE_TAB; + isDropdownOpen: WritableSignal = signal(false); + isTabDropdownShown: WritableSignal = signal(false); + selectedVersion = ''; + showPopup!: boolean; + isMobileMode = signal(false); + installationCount = 0; + + @HostListener('window:popstate', ['$event']) + onPopState() { + this.activeTab = window.location.hash.split('#tab-')[1]; + if (this.activeTab === undefined) { + this.activeTab = DEFAULT_ACTIVE_TAB; + } + this.updateDropdownSelection(); + } constructor() { + this.resizeObserver = new ResizeObserver(() => { + this.updateDropdownSelection(); + }); + } + + ngOnInit(): void { const productId = this.route.snapshot.params['id']; + this.productDetailService.productId.set(productId); if (productId) { - this.productId = productId; - this.productService.getProductById(productId).subscribe(product => { - this.product = product; + this.productService + .getProductDetails(productId) + .subscribe(productDetail => { + this.productDetail.set(productDetail); + this.productModuleContent.set(productDetail.productModuleContent); + this.productDetailService.productNames.set(productDetail.names); + localStorage.removeItem(STORAGE_ITEM); + this.installationCount = productDetail.installationCount; + }); + this.productFeedbackService.initFeedbacks(); + } + + const savedTab = localStorage.getItem(STORAGE_ITEM); + if (savedTab) { + this.activeTab = savedTab; + } + this.updateDropdownSelection(); + } + + getContent(value: string): boolean { + const content = this.productModuleContent(); + const conditions: { [key: string]: boolean } = { + description: content.description != null, + demo: content.demo != null && content.demo !== '', + setup: content.setup != null && content.setup !== '', + dependency: content.isDependency + }; + + return conditions[value] ?? false; + } + + loadDetailTabs(selectedVersion: string) { + const tag = + selectedVersion.replaceAll('Version ', 'v') || + this.productDetail().newestReleaseVersion; + this.productService + .getProductDetailsWithVersion(this.productDetail().id, tag) + .subscribe(updatedProductDetail => { + this.productModuleContent.set( + updatedProductDetail.productModuleContent + ); }); + } + + onTabChange(event: Event) { + const selectedTab = (event.target as HTMLSelectElement).value; + this.setActiveTab(selectedTab); + this.isTabDropdownShown.update(value => !value); + this.onTabDropdownShown(); + } + + updateDropdownSelection() { + const dropdown = document.getElementById( + 'tab-group-dropdown' + ) as HTMLSelectElement; + if (dropdown) { + dropdown.value = this.activeTab; } } -} \ No newline at end of file + + setActiveTab(tab: string) { + this.activeTab = tab; + const hash = '#tab-' + tab; + const path = window.location.pathname; + if (history.pushState) { + history.pushState(null, '', path + hash); + } else { + window.location.hash = hash; + } + this.updateDropdownSelection(); + + localStorage.setItem(STORAGE_ITEM, tab); + } + + onShowInfoContent() { + this.isDropdownOpen.update(value => !value); + } + + onTabDropdownShown() { + this.isTabDropdownShown.set(!this.isTabDropdownShown()); + } + + @HostListener('document:click', ['$event']) + handleClickOutside(event: MouseEvent) { + if ( + !this.elementRef.nativeElement + .querySelector('.form-select') + .contains(event.target) && + this.isTabDropdownShown() + ) { + this.onTabDropdownShown(); + } + } + + ngAfterViewInit(): void { + this.checkMediaSize(); + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.checkMediaSize(); + } + + checkMediaSize() { + const mediaQuery = window.matchMedia('(max-width: 767px)'); + if (mediaQuery.matches) { + this.isMobileMode.set(true); + } else { + this.isMobileMode.set(false); + } + } + + onClickRateBtn() { + const productId = this.productDetailService.productId(); + if (this.authService.getToken()) { + this.appModalService.openAddFeedbackDialog(); + } else { + this.authService.redirectToGitHub(productId); + } + } + + receiveInstallationCountData(data: number) { + this.installationCount = data; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts new file mode 100644 index 000000000..70a8b44da --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts @@ -0,0 +1,38 @@ +import { TestBed } from '@angular/core/testing'; +import { ProductDetailService } from './product-detail.service'; +import { DisplayValue } from '../../../shared/models/display-value.model'; + +describe('ProductDetailService', () => { + let service: ProductDetailService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductDetailService] + }); + service = TestBed.inject(ProductDetailService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should have a default productId signal', () => { + expect(service.productId()).toBe(''); + }); + + it('should update productId signal', () => { + const newProductId = '12345'; + service.productId.set(newProductId); + expect(service.productId()).toBe(newProductId); + }); + + it('should have a default productNames signal', () => { + expect(service.productNames()).toEqual({} as DisplayValue); + }); + + it('should update productNames signal', () => { + const newProductNames: DisplayValue = { en: 'en', de: 'de' }; + service.productNames.set(newProductNames); + expect(service.productNames()).toEqual(newProductNames); + }); +}); \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts new file mode 100644 index 000000000..8272d25d2 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts @@ -0,0 +1,10 @@ +import { Injectable, signal, WritableSignal } from '@angular/core'; +import { DisplayValue } from '../../../shared/models/display-value.model'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductDetailService { + productId: WritableSignal = signal(''); + productNames: WritableSignal = signal({} as DisplayValue); +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html new file mode 100644 index 000000000..60b474ae8 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html @@ -0,0 +1,9 @@ +
+

+ {{ 'common.product.detail.installation' | translate }} +

+

{{this.currentInstallationCount}}

+

+ {{ 'common.product.detail.times' | translate }} +

+
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss new file mode 100644 index 000000000..4f230a2d5 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss @@ -0,0 +1,29 @@ +.analysis-title { + line-height: 140%; +} + +.product-analysis { + line-height: 110%; +} + +.analysis-container { + gap: 10px; +} + +@media (max-width: 768px) { + .analysis-container { + gap: 7px; + } + + .analysis-title { + font-size: 1rem; + } + + .product-analysis { + font-size: 38px; + } + + .analysis-title { + font-size: 1rem; + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.spec.ts new file mode 100644 index 000000000..8dfcb422f --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductInstallationCountActionComponent } from './product-installation-count-action.component'; +import {MarkdownModule} from "ngx-markdown"; +import {TranslateModule} from "@ngx-translate/core"; + +describe('ProductInstallationCountActionComponent', () => { + let component: ProductInstallationCountActionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ProductInstallationCountActionComponent, + TranslateModule.forRoot(), + MarkdownModule.forRoot() + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProductInstallationCountActionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts new file mode 100644 index 000000000..6b40a0e5a --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts @@ -0,0 +1,17 @@ +import {Component, Input} from '@angular/core'; +import {TranslateModule} from "@ngx-translate/core"; + +@Component({ + selector: 'app-product-installation-count-action', + standalone: true, + imports: [ + TranslateModule + ], + templateUrl: './product-installation-count-action.component.html', + styleUrl: './product-installation-count-action.component.scss' +}) + +export class ProductInstallationCountActionComponent { + @Input() + currentInstallationCount!: number; +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html new file mode 100644 index 000000000..a669dd5f7 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html @@ -0,0 +1,46 @@ +
+

+ {{ + (productStarRatingService.totalComments() > 0 + ? 'common.feedback.reviewLabel' + : 'common.feedback.reviewLabelNoYet' + ) | translate + }} +

+ @if (productStarRatingService.totalComments() > 0) { +

+ {{ productStarRatingService.reviewNumber() | number: '1.1-1' }} +

+ } @else { + Message Star + } +
+ + @if (isShowTotalRatingNumber) { +

+ ({{ productStarRatingService.totalComments() }}) +

+ } +
+ @if (isShowRateLink) { + + {{ 'common.feedback.rateLinkLabel' | translate }} + + } +
diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss new file mode 100644 index 000000000..10b3f402d --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss @@ -0,0 +1,40 @@ +.total-rating-number { + font-size: 64px; +} + +.rate-link { + cursor: pointer; + color: var(--ivy-active-color); + font-size: 14px; + font-weight: 400; + line-height: 16.8px; +} + +.star-rating-min-width { + min-width: 180px; +} + +.message-star-rating-img { + width: 72px; + height: 72px; + margin-bottom: 8px; + margin-top: 2px +} + +@media (max-width: 767px) { + .total-rating-number-detail-page { + font-size: 38px; + } + + .review-label-detail-page { + margin-bottom: 6px; + font-size: 1rem; + } + + .message-star-rating-img { + width: 42px; + height: 42px; + margin-bottom: 4px; + } +} + diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts new file mode 100644 index 000000000..41244dcba --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts @@ -0,0 +1,78 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProductStarRatingNumberComponent } from './product-star-rating-number.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { StarRatingComponent } from '../../../../shared/components/star-rating/star-rating.component'; +import { ProductStarRatingService } from '../product-detail-feedback/product-star-rating-panel/product-star-rating.service'; +import { AuthService } from '../../../../auth/auth.service'; +import { ProductDetailService } from '../product-detail.service'; +import { By } from '@angular/platform-browser'; +import { of } from 'rxjs'; + +describe('ProductStarRatingNumberComponent', () => { + let component: ProductStarRatingNumberComponent; + let fixture: ComponentFixture; + let mockProductStarRatingService: jasmine.SpyObj; + let mockProductDetailService: jasmine.SpyObj; + let mockAuthService: jasmine.SpyObj; + + beforeEach(async () => { + mockProductStarRatingService = jasmine.createSpyObj('ProductStarRatingService', ['reviewNumber', 'totalComments']); + mockProductDetailService = jasmine.createSpyObj('ProductDetailService', ['productId']); + mockAuthService = jasmine.createSpyObj('AuthService', ['getToken', 'redirectToGitHub']); + + await TestBed.configureTestingModule({ + imports: [ProductStarRatingNumberComponent, StarRatingComponent, TranslateModule.forRoot()], + providers: [ + { provide: ProductStarRatingService, useValue: mockProductStarRatingService }, + { provide: ProductDetailService, useValue: mockProductDetailService }, + { provide: AuthService, useValue: mockAuthService } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductStarRatingNumberComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should inject services', () => { + expect(component['productStarRatingService']).toBeDefined(); + expect(component['productDetailService']).toBeDefined(); + expect(component['authService']).toBeDefined(); + }); + + it('should emit openAddFeedbackDialog event if user is authenticated', () => { + mockAuthService.getToken.and.returnValue('mockToken'); + spyOn(component.openAddFeedbackDialog, 'emit'); + const link = fixture.debugElement.query(By.css('.rate-link')).nativeElement; + link.click(); + expect(component.openAddFeedbackDialog.emit).toHaveBeenCalled(); + }); + + it('should redirect to GitHub if user is not authenticated', () => { + mockAuthService.getToken.and.returnValue(null); + mockProductDetailService.productId.and.returnValue('123'); + const link = fixture.debugElement.query(By.css('.rate-link')).nativeElement; + link.click(); + expect(mockAuthService.redirectToGitHub).toHaveBeenCalledWith('123'); + }); + + it('should render star rating and review number', () => { + mockProductStarRatingService.reviewNumber.and.returnValue(4.5); + mockProductStarRatingService.totalComments.and.returnValue(10); + fixture.detectChanges(); + + const reviewNumber = fixture.debugElement.query(By.css('.total-rating-number')).nativeElement; + const totalComments = fixture.debugElement.query(By.css('h4.d-inline-block')).nativeElement; + const starRatingComponent = fixture.debugElement.query(By.directive(StarRatingComponent)); + + expect(reviewNumber.textContent).toContain('4.5'); + expect(totalComments.textContent).toContain('(10)'); + expect(starRatingComponent).toBeTruthy(); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts new file mode 100644 index 000000000..4516546ff --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts @@ -0,0 +1,36 @@ +import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { StarRatingComponent } from '../../../../shared/components/star-rating/star-rating.component'; +import { + ProductStarRatingService +} from '../product-detail-feedback/product-star-rating-panel/product-star-rating.service'; +import { CommonModule } from '@angular/common'; +import { AuthService } from '../../../../auth/auth.service'; +import { ProductDetailService } from '../product-detail.service'; + +@Component({ + selector: 'app-product-star-rating-number', + standalone: true, + imports: [CommonModule, TranslateModule, StarRatingComponent], + templateUrl: './product-star-rating-number.component.html', + styleUrl: './product-star-rating-number.component.scss' +}) +export class ProductStarRatingNumberComponent { + productStarRatingService = inject(ProductStarRatingService); + private readonly productDetailService = inject(ProductDetailService); + private readonly authService = inject(AuthService); + + @Input() isShowRateLink = true; + @Input() isShowTotalRatingNumber = true; + @Output() openAddFeedbackDialog = new EventEmitter(); + + onClickRateLink() { + const productId = this.productDetailService.productId(); + if(this.authService.getToken()) { + this.openAddFeedbackDialog.emit(); + } + else { + this.authService.redirectToGitHub(productId); + } + } +} diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html index 519f6db4c..e7679f254 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html @@ -6,10 +6,10 @@
@for (type of types; track $index) { -
-

- {{ type.label | translate }} -

-
+

+ {{ type.label | translate }} +

+
}
@@ -31,9 +31,9 @@ aria-label="sort" name="sort"> @for (type of types; track $index) { - + }
@@ -51,9 +51,9 @@

aria-label="sort" name="sort"> @for (type of sorts; track $index) { - + }

@@ -74,6 +74,6 @@

class="form-control input__search bg-secondary border-0 rounded-end-4 rounded-start-0 search-input" [placeholder]="translateService.get('common.search.placeholder') | async" [ariaLabel]="translateService.get('common.search.placeholder') | async" - aria-describedby="search" /> + aria-describedby="search"/> diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts index d8730c47a..8961dc021 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts @@ -1,12 +1,9 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Output, inject } from '@angular/core'; +import { Component, EventEmitter, inject, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { ThemeService } from '../../../core/services/theme/theme.service'; -import { - FILTER_TYPES, - SORT_TYPES -} from '../../../shared/constants/common.constant'; +import { FILTER_TYPES, SORT_TYPES } from '../../../shared/constants/common.constant'; import { TypeOption } from '../../../shared/enums/type-option.enum'; import { SortOption } from '../../../shared/enums/sort-option.enum'; diff --git a/marketplace-ui/src/app/modules/product/product.component.html b/marketplace-ui/src/app/modules/product/product.component.html index efe03108d..3a5949fed 100644 --- a/marketplace-ui/src/app/modules/product/product.component.html +++ b/marketplace-ui/src/app/modules/product/product.component.html @@ -29,7 +29,7 @@

@for (product of products(); track $index) {
diff --git a/marketplace-ui/src/app/modules/product/product.component.spec.ts b/marketplace-ui/src/app/modules/product/product.component.spec.ts index 071c54229..920d2b603 100644 --- a/marketplace-ui/src/app/modules/product/product.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.component.spec.ts @@ -1,9 +1,4 @@ -import { - ComponentFixture, - TestBed, - fakeAsync, - tick -} from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; @@ -25,12 +20,18 @@ describe('ProductComponent', () => { let mockIntersectionObserver: any; beforeAll(() => { - mockIntersectionObserver = jasmine.createSpyObj('IntersectionObserver', ['observe', 'unobserve', 'disconnect']); - mockIntersectionObserver.observe.and.callFake(() => { }); - mockIntersectionObserver.unobserve.and.callFake(() => { }); - mockIntersectionObserver.disconnect.and.callFake(() => { }); - - (window as any).IntersectionObserver = function (callback: IntersectionObserverCallback) { + mockIntersectionObserver = jasmine.createSpyObj('IntersectionObserver', [ + 'observe', + 'unobserve', + 'disconnect' + ]); + mockIntersectionObserver.observe.and.callFake(() => {}); + mockIntersectionObserver.unobserve.and.callFake(() => {}); + mockIntersectionObserver.disconnect.and.callFake(() => {}); + + (window as any).IntersectionObserver = function ( + callback: IntersectionObserverCallback + ) { mockIntersectionObserver.callback = callback; return mockIntersectionObserver; }; @@ -69,16 +70,10 @@ describe('ProductComponent', () => { expect(component).toBeTruthy(); }); - it('viewProductDetail should navigate', () => { - component.viewProductDetail('url'); - expect(router.navigate).toHaveBeenCalledWith(['', 'url']); - }); - it('loadProductItems should return products with criteria', () => { - component.loadProductItems(); expect(component.loadProductItems).toBeTruthy(); - }) + }); it('ngOnDestroy should unsubscribe all sub', () => { const sub = new Subscription(); @@ -89,7 +84,7 @@ describe('ProductComponent', () => { it('onFilterChange should filter products properly', () => { component.onFilterChange(TypeOption.CONNECTORS); - component.products().forEach((product) => { + component.products().forEach(product => { expect(product.type).toEqual('connector'); }); }); @@ -99,7 +94,9 @@ describe('ProductComponent', () => { component.onSortChange(SortOption.ALPHABETICALLY); for (let i = 0; i < component.products.length - 1; i++) { expect( - component.products()[i + 1].names.en.localeCompare(component.products()[i].names.en) + component + .products() + [i + 1].names['en'].localeCompare(component.products()[i].names['en']) ).toEqual(1); } }); @@ -108,8 +105,8 @@ describe('ProductComponent', () => { const productName = 'amazon comprehend'; component.onSearchChanged(productName); tick(500); - component.products().forEach((product) => { - expect(product.names.en.toLowerCase()).toContain(productName); + component.products().forEach(product => { + expect(product.names['en'].toLowerCase()).toContain(productName); }); })); @@ -156,4 +153,12 @@ describe('ProductComponent', () => { expect(component.hasMore).toHaveBeenCalled(); expect(component.loadProductItems).not.toHaveBeenCalled(); }); + + it('viewProductDetail should navigate', () => { + const productId = 'jira-connector'; + + component.viewProductDetail(productId, ''); + + expect(router.navigate).toHaveBeenCalledWith(['', productId]); + }); }); diff --git a/marketplace-ui/src/app/modules/product/product.component.ts b/marketplace-ui/src/app/modules/product/product.component.ts index 00ae84665..3117e6d7b 100644 --- a/marketplace-ui/src/app/modules/product/product.component.ts +++ b/marketplace-ui/src/app/modules/product/product.component.ts @@ -3,16 +3,16 @@ import { AfterViewInit, Component, ElementRef, + inject, OnDestroy, + signal, ViewChild, - WritableSignal, - inject, - signal + WritableSignal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; +import { NavigationStart, Router } from '@angular/router'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Subject, Subscription, debounceTime } from 'rxjs'; +import { debounceTime, Subject, Subscription } from 'rxjs'; import { ThemeService } from '../../core/services/theme/theme.service'; import { TypeOption } from '../../shared/enums/type-option.enum'; import { SortOption } from '../../shared/enums/sort-option.enum'; @@ -25,6 +25,7 @@ import { ProductApiResponse } from '../../shared/models/apis/product-response.mo import { Link } from '../../shared/models/apis/link.model'; import { Page } from '../../shared/models/apis/page.model'; import { Language } from '../../shared/enums/language.enum'; +import { ProductDetail } from '../../shared/models/product-detail.model'; import { LanguageService } from '../../core/services/language/language.service'; const SEARCH_DEBOUNCE_TIME = 500; @@ -45,6 +46,7 @@ const SEARCH_DEBOUNCE_TIME = 500; }) export class ProductComponent implements AfterViewInit, OnDestroy { products: WritableSignal = signal([]); + productDetail!: ProductDetail; subscriptions: Subscription[] = []; searchTextChanged = new Subject(); criteria: Criteria = { @@ -77,13 +79,19 @@ export class ProductComponent implements AfterViewInit, OnDestroy { this.loadProductItems(true); }) ); + this.router.events?.subscribe(event => { + if (!(event instanceof NavigationStart)) { + return; + } + window.scrollTo(0, 0); + }); } ngAfterViewInit(): void { this.setupIntersectionObserver(); } - viewProductDetail(productId: string) { + viewProductDetail(productId: string, _productTag: string) { this.router.navigate(['', productId]); } @@ -110,18 +118,22 @@ export class ProductComponent implements AfterViewInit, OnDestroy { } loadProductItems(shouldCleanData = false) { - this.criteria.language = this.languageService.getSelectedLanguage(); + this.criteria.language = this.languageService.selectedLanguage(); this.subscriptions.push( - this.productService.findProductsByCriteria(this.criteria).subscribe((response: ProductApiResponse) => { - const newProducts = response._embedded.products; - if (shouldCleanData) { - this.products.set(newProducts); - } else { - this.products.update(existingProducts => existingProducts.concat(newProducts)); - } - this.responseLink = response._links; - this.responsePage = response.page; - }) + this.productService + .findProductsByCriteria(this.criteria) + .subscribe((response: ProductApiResponse) => { + const newProducts = response._embedded.products; + if (shouldCleanData) { + this.products.set(newProducts); + } else { + this.products.update(existingProducts => + existingProducts.concat(newProducts) + ); + } + this.responseLink = response._links; + this.responsePage = response.page; + }) ); } @@ -143,8 +155,10 @@ export class ProductComponent implements AfterViewInit, OnDestroy { if (!this.responsePage || !this.responseLink) { return false; } - return this.responsePage.number < this.responsePage.totalPages - && this.responseLink?.next !== undefined; + return ( + this.responsePage.number < this.responsePage.totalPages && + this.responseLink?.next !== undefined + ); } ngOnDestroy(): void { diff --git a/marketplace-ui/src/app/modules/product/product.routes.ts b/marketplace-ui/src/app/modules/product/product.routes.ts index c4fa88ebd..157cb6fd1 100644 --- a/marketplace-ui/src/app/modules/product/product.routes.ts +++ b/marketplace-ui/src/app/modules/product/product.routes.ts @@ -5,7 +5,7 @@ export const routes: Route[] = [ path: '', loadComponent: () => import('./product-detail/product-detail.component').then( - (m) => m.ProductDetailComponent - ), - }, + m => m.ProductDetailComponent + ) + } ]; diff --git a/marketplace-ui/src/app/modules/product/product.service.spec.ts b/marketplace-ui/src/app/modules/product/product.service.spec.ts index df0deed1f..7908663bb 100644 --- a/marketplace-ui/src/app/modules/product/product.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.service.spec.ts @@ -1,29 +1,21 @@ import { TestBed } from '@angular/core/testing'; -import { - provideHttpClient, - withInterceptorsFromDi -} from '@angular/common/http'; -import { - HttpTestingController, - provideHttpClientTesting -} from '@angular/common/http/testing'; -import { TypeOption } from '../../shared/enums/type-option.enum'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { LoadingService } from '../../core/services/loading/loading.service'; +import { Language } from '../../shared/enums/language.enum'; import { SortOption } from '../../shared/enums/sort-option.enum'; -import { MOCK_PRODUCTS } from '../../shared/mocks/mock-data'; +import { TypeOption } from '../../shared/enums/type-option.enum'; +import { + MOCK_PRODUCTS, + MOCK_PRODUCT_DETAILS +} from '../../shared/mocks/mock-data'; import { Criteria } from '../../shared/models/criteria.model'; -import { ProductService } from './product.service'; -import { Product } from '../../shared/models/product.model'; -import { catchError } from 'rxjs'; -import { LoadingService } from '../../core/services/loading/loading.service'; import { VersionData } from '../../shared/models/vesion-artifact.model'; -import { Language } from '../../shared/enums/language.enum'; - -const PRODUCT_ID = 'amazon-comprehend'; -const NOT_EXIST_ID = 'undefined'; +import { ProductService } from './product.service'; describe('ProductService', () => { - let products = MOCK_PRODUCTS._embedded.products as Product[]; + let products = MOCK_PRODUCTS._embedded.products; let service: ProductService; let httpMock: HttpTestingController; let loadingServiceSpy: jasmine.SpyObj; @@ -51,18 +43,6 @@ describe('ProductService', () => { expect(service).toBeTruthy(); }); - it('getProductById should return a product', () => { - service.getProductById(PRODUCT_ID).subscribe(data => { - expect(data.id).toEqual(PRODUCT_ID); - }); - }); - - it('getProductById should return null product', () => { - service.getProductById(NOT_EXIST_ID).subscribe(data => { - expect(data).toEqual({} as Product); - }); - }); - it('findProductsByCriteria with should return products properly', () => { const searchString = 'Amazon Comprehend'; const criteria: Criteria = { @@ -75,11 +55,11 @@ describe('ProductService', () => { let products = response._embedded.products; for (let i = 0; i < products.length; i++) { expect(products[i].type).toEqual(TypeOption.CONNECTORS); - expect(products[i].names.en.toLowerCase()).toContain(searchString); + expect(products[i].names['en'].toLowerCase()).toContain(searchString); if (products[i + 1]) { - expect(products[i + 1].names.en.localeCompare(products[i].names.en)).toEqual( - 1 - ); + expect( + products[i + 1].names['en'].localeCompare(products[i].names['en']) + ).toEqual(1); } } }); @@ -180,4 +160,32 @@ describe('ProductService', () => { expect(loadingServiceSpy.show).toHaveBeenCalled(); expect(loadingServiceSpy.hide).toHaveBeenCalled(); }); + + it('getProductDetailsWithVersion should return a product detail', () => { + const productId = 'jira-connector'; + const tag = 'v10.0.10'; + + service.getProductDetailsWithVersion(productId, tag).subscribe(data => { + expect(data).toEqual(MOCK_PRODUCT_DETAILS); + }); + + const req = httpMock.expectOne(request => { + expect(request.url).toEqual(`api/product-details/${productId}/${tag}`); + + return true; + }); + }); + + it('sendRequestToUpdateInstallationCount', () => { + const productId = "google-maps-connector"; + + service.sendRequestToUpdateInstallationCount(productId).subscribe(response => { + expect(response).toBe(3); + }); + + const req = httpMock.expectOne(`api/product-details/installationcount/${productId}`); + expect(req.request.method).toBe('PUT'); + expect(req.request.headers.get('X-Requested-By')).toBe('ivy'); + req.flush(3); + }) }); diff --git a/marketplace-ui/src/app/modules/product/product.service.ts b/marketplace-ui/src/app/modules/product/product.service.ts index fac5ed24c..3d93052a5 100644 --- a/marketplace-ui/src/app/modules/product/product.service.ts +++ b/marketplace-ui/src/app/modules/product/product.service.ts @@ -1,14 +1,12 @@ -import { HttpClient, HttpContext, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; -import { Observable, of, tap } from 'rxjs'; -import { MOCK_PRODUCTS } from '../../shared/mocks/mock-data'; -import { Criteria } from '../../shared/models/criteria.model'; -import { Product } from '../../shared/models/product.model'; -import { VersionData } from '../../shared/models/vesion-artifact.model'; +import { Observable, tap } from 'rxjs'; import { LoadingService } from '../../core/services/loading/loading.service'; import { RequestParam } from '../../shared/enums/request-param'; import { ProductApiResponse } from '../../shared/models/apis/product-response.model'; -import { SkipLoading } from '../../core/interceptors/api.interceptor'; +import { Criteria } from '../../shared/models/criteria.model'; +import { ProductDetail } from '../../shared/models/product-detail.model'; +import { VersionData } from '../../shared/models/vesion-artifact.model'; const PRODUCT_API_URL = 'api/product'; @Injectable() @@ -29,18 +27,23 @@ export class ProductService { .set(RequestParam.LANGUAGE, `${criteria.language}`); } return this.httpClient.get(requestURL, { - params: requestParams, - context: new HttpContext().set(SkipLoading, true) + params: requestParams }); } - getProductById(productId: string): Observable { - const products = MOCK_PRODUCTS._embedded.products; - const product = products.find(p => p.id === productId); - if (product) { - return of(product); - } - return of({} as Product); + getProductDetailsWithVersion( + productId: string, + tag: string + ): Observable { + return this.httpClient.get( + `api/product-details/${productId}/${tag}` + ); + } + + getProductDetails(productId: string): Observable { + return this.httpClient.get( + `api/product-details/${productId}` + ); } sendRequestToProductDetailVersionAPI( @@ -59,4 +62,9 @@ export class ProductService { }) ); } + + sendRequestToUpdateInstallationCount(productId: string) { + const url = 'api/product-details/installationcount/' + productId; + return this.httpClient.put(url, null, { headers: { 'X-Requested-By': 'ivy' } }); + } } diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.html b/marketplace-ui/src/app/shared/components/footer/footer.component.html index 53941ff9e..9f215e2ba 100644 --- a/marketplace-ui/src/app/shared/components/footer/footer.component.html +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.html @@ -13,7 +13,7 @@ ? '/assets/images/misc/axonivy-logo.svg' : '/assets/images/misc/axonivy-logo-black.svg' " - alt="Axon Ivy" /> + alt="Axon Ivy"/>
@@ -58,11 +58,11 @@ @@ -72,14 +72,14 @@ diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.ts b/marketplace-ui/src/app/shared/components/footer/footer.component.ts index 4a3d088c0..3ae857a18 100644 --- a/marketplace-ui/src/app/shared/components/footer/footer.component.ts +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.ts @@ -2,11 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component, inject } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { ThemeService } from '../../../core/services/theme/theme.service'; -import { - IVY_FOOTER_LINKS, - NAV_ITEMS, - SOCIAL_MEDIA_LINK -} from '../../constants/common.constant'; +import { IVY_FOOTER_LINKS, NAV_ITEMS, SOCIAL_MEDIA_LINK } from '../../constants/common.constant'; import { NavItem } from '../../models/nav-item.model'; @Component({ diff --git a/marketplace-ui/src/app/shared/components/header/header.component.html b/marketplace-ui/src/app/shared/components/header/header.component.html index b589d0446..389ce3784 100644 --- a/marketplace-ui/src/app/shared/components/header/header.component.html +++ b/marketplace-ui/src/app/shared/components/header/header.component.html @@ -8,7 +8,7 @@ ? '/assets/images/misc/axonivy-logo.svg' : '/assets/images/misc/axonivy-logo-black.svg' " - alt="Axon Ivy" /> + alt="Axon Ivy"/> - + + } diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts index 2565dbb1a..caf326ec2 100644 --- a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts @@ -1,14 +1,5 @@ import { CommonModule } from '@angular/common'; -import { - Component, - ElementRef, - EventEmitter, - HostListener, - Input, - Output, - inject, - signal -} from '@angular/core'; +import { Component, ElementRef, HostListener, inject, signal } from '@angular/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { LanguageSelectionComponent } from '../language-selection/language-selection.component'; import { ThemeSelectionComponent } from '../theme-selection/theme-selection.component'; @@ -26,11 +17,9 @@ import { ThemeSelectionComponent } from '../theme-selection/theme-selection.comp styleUrl: './search-bar.component.scss' }) export class SearchBarComponent { - @Input() isSearchBarDisplayed = signal(false); - @Output() isShowSearchBarChange = new EventEmitter(); + isSearchBarDisplayed = signal(false); translateService = inject(TranslateService); - elementRef = inject(ElementRef); @HostListener('document:click', ['$event']) diff --git a/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.html b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.html new file mode 100644 index 000000000..85f843feb --- /dev/null +++ b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.html @@ -0,0 +1,13 @@ + + + + @if (fill > 0) { + + } + + + \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.scss b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.scss new file mode 100644 index 000000000..2e994a517 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.scss @@ -0,0 +1,45 @@ +.star-feedback { + position: relative; + display: inline-block; + color: var(--star-color); + font-size: 10px; + margin-right: 3px; + .filled { + color: var(--star-filled-color); + overflow: hidden; + position: absolute; + top: 0; + left: 0; + } +} + +.medium-star { + font-size: 20px; + margin-right: 8px; +} + +.adding-feedback-star { + font-size: 2.5rem; + color: #dfdfdf; + margin-right: 15px; + .filled { + color: #FFCB13; + } +} + +@media (max-width: 767px) { + .medium-star { + font-size: 16px; + margin-right: 8px; + } + + .adding-feedback-star { + margin-right: 39px; + } +} + +@media (max-width: 400px) { + .adding-feedback-star { + margin-right: clamp(27px, 6vw, 39px); + } +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.spec.ts b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.spec.ts new file mode 100644 index 000000000..f12398c4b --- /dev/null +++ b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StarRatingComponent } from './star-rating.component'; +import { By } from '@angular/platform-browser'; +import { NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; + +describe('StarRatingComponent', () => { + let component: StarRatingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StarRatingComponent, NgbRatingModule] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StarRatingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with default inputs', () => { + expect(component.rate).toBe(0); + expect(component.isReadOnly).toBe(false); + expect(component.starClass).toBe(''); + }); + + it('should display the correct number of stars', () => { + component.rate = 3; + fixture.detectChanges(); + + const stars = fixture.debugElement.queryAll(By.css('.star-feedback')); + expect(stars.length).toBe(5); // assuming a 5-star rating system + const filledStars = fixture.debugElement.queryAll(By.css('.filled')); + expect(filledStars.length).toBe(3); + }); + + it('should emit rateChange event when rate changes', () => { + spyOn(component.rateChange, 'emit'); + + component.onRateChange(4); + + expect(component.rate).toBe(4); + expect(component.rateChange.emit).toHaveBeenCalledWith(4); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.ts b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.ts new file mode 100644 index 000000000..477ad1ae1 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/star-rating/star-rating.component.ts @@ -0,0 +1,22 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { NgbRating } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-star-rating', + standalone: true, + imports: [NgbRating], + templateUrl: './star-rating.component.html', + styleUrl: './star-rating.component.scss' +}) +export class StarRatingComponent { + @Input() rate = 0; + @Input() isReadOnly = false; + @Input() starClass = ''; + + @Output() rateChange = new EventEmitter(); + + onRateChange(newRate: number): void { + this.rate = newRate; + this.rateChange.emit(newRate); + } +} diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts index 968d80da2..9df949ebb 100644 --- a/marketplace-ui/src/app/shared/constants/common.constant.ts +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -1,7 +1,9 @@ import { TypeOption } from '../enums/type-option.enum'; +import { FeedbackSortType } from '../enums/feedback-sort-type'; import { Language } from '../enums/language.enum'; import { SortOption } from '../enums/sort-option.enum'; import { NavItem } from '../models/nav-item.model'; +import { DetailTab } from '../../modules/product/product-detail/product-detail.component'; export const NAV_ITEMS: NavItem[] = [ { @@ -108,3 +110,53 @@ export const SORT_TYPES = [ label: 'common.sort.value.recent' } ]; + +export const PRODUCT_DETAIL_TABS: DetailTab[] = [ + { + activeClass: "activeTab === 'description'", + tabId: 'description-tab', + value: 'description', + label: 'common.product.detail.description' + }, + { + activeClass: "activeTab === 'demo'", + tabId: 'demo-tab', + value: 'demo', + label: 'common.product.detail.demo' + }, + { + activeClass: "activeTab === 'setup'", + tabId: 'setup-tab', + value: 'setup', + label: 'common.product.detail.installationGuide' + }, + { + activeClass: "activeTab === 'dependency'", + tabId: 'dependency-tab', + value: 'dependency', + label: 'common.product.detail.maven.label' + } +]; + +export const FEEDBACK_SORT_TYPES = [ + { + value: FeedbackSortType.NEWEST, + label: 'common.sort.value.newest', + sortFn: 'updatedAt,desc' + }, + { + value: FeedbackSortType.OLDEST, + label: 'common.sort.value.oldest', + sortFn: 'updatedAt,asc' + }, + { + value: FeedbackSortType.HIGHEST, + label: 'common.sort.value.highest', + sortFn: 'rating,desc' + }, + { + value: FeedbackSortType.LOWEST, + label: 'common.sort.value.lowest', + sortFn: 'rating,asc' + } +]; \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/enums/feedback-sort-type.ts b/marketplace-ui/src/app/shared/enums/feedback-sort-type.ts new file mode 100644 index 000000000..754c4b1a6 --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/feedback-sort-type.ts @@ -0,0 +1,6 @@ +export enum FeedbackSortType { + NEWEST = 'newest', + OLDEST = 'oldest', + HIGHEST = 'highest', + LOWEST = 'lowest' +} diff --git a/marketplace-ui/src/app/shared/mocks/mock-data.ts b/marketplace-ui/src/app/shared/mocks/mock-data.ts index e8e43065d..197d66574 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-data.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-data.ts @@ -1,26 +1,27 @@ -import { ProductApiResponse } from "../models/apis/product-response.model"; +import { ProductApiResponse } from '../models/apis/product-response.model'; +import { ProductDetail } from '../models/product-detail.model'; +import { ProductModuleContent } from '../models/product-module-content.model'; export const MOCK_PRODUCTS = { _embedded: { products: [ { - id: "amazon-comprehend", + id: 'amazon-comprehend', names: { - en: "Amazon Comprehend", - de: "TODO Amazon Comprehend" + en: 'Amazon Comprehend', + de: 'TODO Amazon Comprehend' }, shortDescriptions: { - en: "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", - de: "Amazon Comprehend ist ein KI-Service, der maschinelles Lernen nutzt, um aus unstrukturierten Daten wertvolle Informationen zu generieren." + en: 'Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.', + de: 'Amazon Comprehend ist ein KI-Service, der maschinelles Lernen nutzt, um aus unstrukturierten Daten wertvolle Informationen zu generieren.' }, - logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend/logo.png", - type: "connector", - tags: [ - "AI" - ], + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend/logo.png', + type: 'connector', + tags: ['AI'], _links: { self: { - href: "http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector" + href: 'http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector' } } } @@ -28,16 +29,16 @@ export const MOCK_PRODUCTS = { }, _links: { first: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, self: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, next: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20' }, last: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20' } }, page: { @@ -46,29 +47,27 @@ export const MOCK_PRODUCTS = { totalPages: 4, number: 0 } -} as ProductApiResponse; +} as unknown as ProductApiResponse; export const MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS = { _embedded: { products: [ { - id: "amazon-comprehend", + id: 'amazon-comprehend', names: { - en: "Amazon Comprehend", - de: "" + en: 'Amazon Comprehend', + de: '' }, shortDescriptions: { - en: "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", - de: "" + en: 'Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.', + de: '' }, - logoUrl: "", - type: "connector", - tags: [ - "AI" - ], + logoUrl: '', + type: 'connector', + tags: ['AI'], _links: { self: { - href: "http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector" + href: 'http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector' } } } @@ -76,16 +75,16 @@ export const MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS = { }, _links: { first: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, self: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, next: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20' }, last: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20' } }, page: { @@ -94,72 +93,68 @@ export const MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS = { totalPages: 4, number: 0 } -} as ProductApiResponse; +} as unknown as ProductApiResponse; export const MOCK_PRODUCTS_FILTER_CONNECTOR = { _embedded: { products: [ { - id: "amazon-comprehend", + id: 'amazon-comprehend', names: { - en: "Amazon Comprehend", - de: "TODO Amazon Comprehend" + en: 'Amazon Comprehend', + de: 'TODO Amazon Comprehend' }, shortDescriptions: { - en: "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.", - de: "Amazon Comprehend ist ein KI-Service, der maschinelles Lernen nutzt, um aus unstrukturierten Daten wertvolle Informationen zu generieren." + en: 'Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.', + de: 'Amazon Comprehend ist ein KI-Service, der maschinelles Lernen nutzt, um aus unstrukturierten Daten wertvolle Informationen zu generieren.' }, - logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend/logo.png", - type: "connector", - tags: [ - "AI" - ], + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend/logo.png', + type: 'connector', + tags: ['AI'], _links: { self: { - href: "http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector" + href: 'http://localhost:8080/marketplace-service/api/product-details/amazon-comprehend?type=connector' } } }, { - id: "a-trust", + id: 'a-trust', names: { - en: "A-Trust", - de: "A-Trust" + en: 'A-Trust', + de: 'A-Trust' }, shortDescriptions: { - en: "Clearly authenticate your Austrian customers with a mobile phone signature.", - de: "Clearly authenticate your Austrian customers with a mobile phone signature." + en: 'Clearly authenticate your Austrian customers with a mobile phone signature.', + de: 'Clearly authenticate your Austrian customers with a mobile phone signature.' }, - logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/a-trust/logo.png", - type: "connector", - tags: [ - "e-signature" - ], + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/a-trust/logo.png', + type: 'connector', + tags: ['e-signature'], _links: { self: { - href: "http://localhost:8080/marketplace-service/api/product-details/a-trust?type=connector" + href: 'http://localhost:8080/marketplace-service/api/product-details/a-trust?type=connector' } } }, { - id: "mailstore-connector", + id: 'mailstore-connector', names: { - en: "Mailstore", - de: "Mailstore" + en: 'Mailstore', + de: 'Mailstore' }, shortDescriptions: { - en: "Enhance business processes by streamlining email management, supporting both IMAP and POP3 with robust SSL encryption.", - de: "Enhance business processes by streamlining email management, supporting both IMAP and POP3 with robust SSL encryption." + en: 'Enhance business processes by streamlining email management, supporting both IMAP and POP3 with robust SSL encryption.', + de: 'Enhance business processes by streamlining email management, supporting both IMAP and POP3 with robust SSL encryption.' }, - logoUrl: "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/mailstore-connector/logo.png", - type: "connector", - tags: [ - "office", - "email" - ], + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/mailstore-connector/logo.png', + type: 'connector', + tags: ['office', 'email'], _links: { self: { - href: "http://localhost:8080/marketplace-service/api/product-details/mailstore-connector?type=connector" + href: 'http://localhost:8080/marketplace-service/api/product-details/mailstore-connector?type=connector' } } } @@ -167,16 +162,16 @@ export const MOCK_PRODUCTS_FILTER_CONNECTOR = { }, _links: { first: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, self: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, next: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20' }, last: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=3&size=20' } }, page: { @@ -185,8 +180,7 @@ export const MOCK_PRODUCTS_FILTER_CONNECTOR = { totalPages: 4, number: 0 } -} as ProductApiResponse; - +} as unknown as ProductApiResponse; export const MOCK_PRODUCTS_NEXT_PAGE = { _embedded: { @@ -194,10 +188,10 @@ export const MOCK_PRODUCTS_NEXT_PAGE = { }, _links: { first: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20' }, self: { - href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + href: 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20' } }, page: { @@ -206,4 +200,64 @@ export const MOCK_PRODUCTS_NEXT_PAGE = { totalPages: 1, number: 1 } -} as ProductApiResponse; \ No newline at end of file +} as ProductApiResponse; + +export const MOCK_PRODUCT_MODULE_CONTENT: ProductModuleContent = { + tag: 'v10.0.10', + description: null, + demo: '', + setup: '', + isDependency: false, + name: 'Jira Connector', + groupId: 'com.axonivy.connector.jira', + artifactId: 'jira-connector', + type: 'iar' +}; + +export const MOCK_PRODUCT_DETAILS: ProductDetail = { + id: 'jira-connector', + names: { + en: 'Atlassian Jira', + de: 'TODO Atlassian Jira' + }, + shortDescriptions: { + en: "Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform.", + de: "TODO Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform." + }, + installationCount: 1, + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/jira/logo.png', + type: 'connector', + tags: ['helper'], + vendor: 'FROX AG', + vendorUrl: 'https://www.frox.ch', + platformReview: '4.5', + newestReleaseVersion: 'v10.0.0', + cost: 'Free', + sourceUrl: 'https://github.com/axonivy-market/jira-connector', + statusBadgeUrl: + 'https://github.com/axonivy-market/jira-connector/actions/workflows/ci.yml/badge.svg', + language: 'English', + industry: 'Cross-Industry', + compatibility: '9.2+', + contactUs: false, + productModuleContent: { + tag: 'v10.0.0', + description: { + en: "Axon Ivy's [Atlassian Jira Connector ](https://www.atlassian.com/software/jira) gives you full power to track issues within your process work. The connector:\n\n- Features three main functionalities (create comment, create issue, and get issue).\n- Provides access to the core API of Atlassian Jira.\n- Supports you with an easy-to-copy demo implementation to reduce your integration effort.\n- Enables low code citizen developers to integrate issue tracking tools without writing a single line of code." + }, + setup: + 'Open the `Config/variables.yaml` in your Axon Ivy Designer and paste the\ncode below and adjust the values to your environment.\n\n```\nVariables:\n\n jira-connector:\n \n # Url to the Jira server\n Url: "https://localhost"\n\n # Username to connect to the Jira server\n Username: "admin"\n\n # Password to connect to the Jira server\n Password: "1234"\n```', + demo: '![jira-connector Demo 1](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-issue.png "Create Jira issue")\n![jira-connector Demo 2](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-comment.png "Craete Jira comment")', + isDependency: true, + name: 'Jira Connector', + groupId: 'com.axonivy.connector.jira', + artifactId: 'jira-connector', + type: 'iar' + }, + _links: { + self: { + href: 'http://localhost:8082/api/product-details/jira-connector?type=connector' + } + } +}; diff --git a/marketplace-ui/src/app/shared/mocks/mock-services.ts b/marketplace-ui/src/app/shared/mocks/mock-services.ts index e7640bdfb..d9553436f 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-services.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-services.ts @@ -1,17 +1,15 @@ import { Observable, of } from 'rxjs'; -import { Product } from '../models/product.model'; import { Criteria } from '../models/criteria.model'; import { TypeOption } from '../enums/type-option.enum'; -import { MOCK_PRODUCTS, MOCK_PRODUCTS_FILTER_CONNECTOR, MOCK_PRODUCTS_NEXT_PAGE } from './mock-data'; +import { + MOCK_PRODUCTS, + MOCK_PRODUCTS_FILTER_CONNECTOR, + MOCK_PRODUCTS_NEXT_PAGE, + MOCK_PRODUCT_DETAILS +} from './mock-data'; import { ProductApiResponse } from '../models/apis/product-response.model'; -const products = MOCK_PRODUCTS._embedded.products as Product[]; export class MockProductService { - - getProductById(id: string) { - return of(products.find(product => product.id === id)); - } - findProductsByCriteria(criteria: Criteria): Observable { let response = MOCK_PRODUCTS; if (criteria.nextPageHref) { @@ -21,4 +19,8 @@ export class MockProductService { } return of(response); } + + getProductDetails(productId: string, tag: string) { + return of(MOCK_PRODUCT_DETAILS); + } } diff --git a/marketplace-ui/src/app/shared/models/apis/feedback-response.model.ts b/marketplace-ui/src/app/shared/models/apis/feedback-response.model.ts new file mode 100644 index 000000000..e4b94cfc2 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/apis/feedback-response.model.ts @@ -0,0 +1,11 @@ +import { Feedback } from '../feedback.model'; +import { Link } from './link.model'; +import { Page } from './page.model'; + +export interface FeedbackApiResponse { + _embedded: { + feedbacks: Feedback[]; + }; + _links: Link; + page: Page; +} diff --git a/marketplace-ui/src/app/shared/models/apis/product-response.model.ts b/marketplace-ui/src/app/shared/models/apis/product-response.model.ts index d9020d593..cf433fc2a 100644 --- a/marketplace-ui/src/app/shared/models/apis/product-response.model.ts +++ b/marketplace-ui/src/app/shared/models/apis/product-response.model.ts @@ -1,6 +1,6 @@ -import { Product } from "../product.model"; -import { Link } from "./link.model"; -import { Page } from "./page.model"; +import { Product } from '../product.model'; +import { Link } from './link.model'; +import { Page } from './page.model'; export interface ProductApiResponse { _embedded: { @@ -8,4 +8,4 @@ export interface ProductApiResponse { }; _links: Link; page: Page; -} \ No newline at end of file +} diff --git a/marketplace-ui/src/app/shared/models/criteria.model.ts b/marketplace-ui/src/app/shared/models/criteria.model.ts index fce18f832..d4cdb5d0c 100644 --- a/marketplace-ui/src/app/shared/models/criteria.model.ts +++ b/marketplace-ui/src/app/shared/models/criteria.model.ts @@ -1,6 +1,7 @@ -import { Language } from "../enums/language.enum"; -import { SortOption } from "../enums/sort-option.enum"; -import { TypeOption } from "../enums/type-option.enum"; +import { Language } from '../enums/language.enum'; +import { SortOption } from '../enums/sort-option.enum'; +import { TypeOption } from '../enums/type-option.enum'; + export interface Criteria { search: string; sort: SortOption | null; diff --git a/marketplace-ui/src/app/shared/models/display-value.model.ts b/marketplace-ui/src/app/shared/models/display-value.model.ts index 84c9d4c2a..a142586a6 100644 --- a/marketplace-ui/src/app/shared/models/display-value.model.ts +++ b/marketplace-ui/src/app/shared/models/display-value.model.ts @@ -1,5 +1,3 @@ export interface DisplayValue { - en: string, - de: string + [key: string]: string; } - diff --git a/marketplace-ui/src/app/shared/models/feedback.model.ts b/marketplace-ui/src/app/shared/models/feedback.model.ts new file mode 100644 index 000000000..68de53fac --- /dev/null +++ b/marketplace-ui/src/app/shared/models/feedback.model.ts @@ -0,0 +1,10 @@ +export interface Feedback { + username?: string; + userAvatarUrl?: string; + userProvider?: string; + createdDate?: Date; + updatedDate?: Date; + content: string; + rating: number; + productId: string; +} diff --git a/marketplace-ui/src/app/shared/models/product-detail.model.ts b/marketplace-ui/src/app/shared/models/product-detail.model.ts new file mode 100644 index 000000000..65fcfcf75 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/product-detail.model.ts @@ -0,0 +1,29 @@ +import { DisplayValue } from './display-value.model'; +import { ProductModuleContent } from './product-module-content.model'; + +export interface ProductDetail { + id: string; + names: DisplayValue; + shortDescriptions: DisplayValue; + logoUrl: string; + type: string; + tags: string[]; + vendor: string; + vendorUrl: string; + platformReview: string; + newestReleaseVersion: string; + cost: string; + sourceUrl: string; + statusBadgeUrl: string; + language: string; + industry: string; + compatibility: string; + contactUs: boolean; + installationCount: number; + productModuleContent: ProductModuleContent; + _links: { + self: { + href: string; + }; + }; +} diff --git a/marketplace-ui/src/app/shared/models/product-module-content.model.ts b/marketplace-ui/src/app/shared/models/product-module-content.model.ts new file mode 100644 index 000000000..ebdf560e0 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/product-module-content.model.ts @@ -0,0 +1,13 @@ +import { DisplayValue } from "./display-value.model"; + +export interface ProductModuleContent { + tag: string; + description: DisplayValue | null; + demo: string; + setup: string; + isDependency: boolean; + name: string; + groupId: string; + artifactId: string; + type: string; +} diff --git a/marketplace-ui/src/app/shared/models/star-rating-counting.model.ts b/marketplace-ui/src/app/shared/models/star-rating-counting.model.ts new file mode 100644 index 000000000..ec08329a7 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/star-rating-counting.model.ts @@ -0,0 +1,5 @@ +export interface StarRatingCounting { + starRating: number; + commentNumber?: number; + percent?: number; +} diff --git a/marketplace-ui/src/app/shared/pipes/icon.pipe.ts b/marketplace-ui/src/app/shared/pipes/icon.pipe.ts new file mode 100644 index 000000000..9615abb60 --- /dev/null +++ b/marketplace-ui/src/app/shared/pipes/icon.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + standalone: true, + name: 'productTypeIcon' +}) +export class ProductTypeIconPipe implements PipeTransform { + transform(value: string, _args?: []): string { + switch (value) { + case 'connector': + return 'bi bi-plug'; + case 'solution': + return 'bi bi-clipboard-check'; + case 'util': + return 'bi bi-tools'; + default: + return 'bi bi-grid'; + } + } +} diff --git a/marketplace-ui/src/app/shared/services/app-modal.service.spec.ts b/marketplace-ui/src/app/shared/services/app-modal.service.spec.ts new file mode 100644 index 000000000..2f0c9eb58 --- /dev/null +++ b/marketplace-ui/src/app/shared/services/app-modal.service.spec.ts @@ -0,0 +1,57 @@ +import { TestBed } from '@angular/core/testing'; + +import { AppModalService } from './app-modal.service'; +import { AddFeedbackDialogComponent } from '../../modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component'; +import { SuccessDialogComponent } from '../../modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component'; +import { ShowFeedbacksDialogComponent } from '../../modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('AppModalService', () => { + let service: AppModalService; + let modalServiceSpy: jasmine.SpyObj; + + beforeEach(() => { + const spy = jasmine.createSpyObj('NgbModal', ['open']); + + TestBed.configureTestingModule({ + providers: [ + AppModalService, + { provide: NgbModal, useValue: spy } + ] + }); + + service = TestBed.inject(AppModalService); + modalServiceSpy = TestBed.inject(NgbModal) as jasmine.SpyObj; + }); + + it('should open ShowFeedbacksDialogComponent with correct options', () => { + service.openShowFeedbacksDialog(); + expect(modalServiceSpy.open).toHaveBeenCalledWith(ShowFeedbacksDialogComponent, { + centered: true, + modalDialogClass: 'show-feedbacks-modal-dialog', + windowClass: 'overflow-hidden' + }); + }); + + it('should open AddFeedbackDialogComponent with correct options and return result', async () => { + const mockResult = Promise.resolve('test result'); + modalServiceSpy.open.and.returnValue({ result: mockResult } as any); + + const result = await service.openAddFeedbackDialog(); + expect(modalServiceSpy.open).toHaveBeenCalledWith(AddFeedbackDialogComponent, { + fullscreen: 'md', + centered: true, + modalDialogClass: 'add-feedback-modal-dialog' + }); + expect(result).toBe('test result'); + }); + + it('should open SuccessDialogComponent with correct options', () => { + service.openSuccessDialog(); + expect(modalServiceSpy.open).toHaveBeenCalledWith(SuccessDialogComponent, { + fullscreen: 'md', + centered: true, + modalDialogClass: 'add-feedback-modal-dialog' + }); + }); +}); diff --git a/marketplace-ui/src/app/shared/services/app-modal.service.ts b/marketplace-ui/src/app/shared/services/app-modal.service.ts new file mode 100644 index 000000000..999b2d5f7 --- /dev/null +++ b/marketplace-ui/src/app/shared/services/app-modal.service.ts @@ -0,0 +1,40 @@ +import { inject, Injectable } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ShowFeedbacksDialogComponent } from '../../modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component'; +import { AddFeedbackDialogComponent } from '../../modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component'; +import { SuccessDialogComponent } from '../../modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component'; + +@Injectable({ + providedIn: 'root' +}) +export class AppModalService { + private readonly modalService = inject(NgbModal); + + openShowFeedbacksDialog() { + this.modalService.open(ShowFeedbacksDialogComponent, { + centered: true, + modalDialogClass: 'show-feedbacks-modal-dialog', + windowClass: 'overflow-hidden' + }); + } + + openAddFeedbackDialog() { + const addFeedbackDialog = this.modalService.open( + AddFeedbackDialogComponent, + { + fullscreen: 'md', + centered: true, + modalDialogClass: 'add-feedback-modal-dialog' + } + ); + return addFeedbackDialog.result; + } + + openSuccessDialog() { + this.modalService.open(SuccessDialogComponent, { + fullscreen: 'md', + centered: true, + modalDialogClass: 'add-feedback-modal-dialog' + }); + } +} diff --git a/marketplace-ui/src/assets/i18n/de.yaml b/marketplace-ui/src/assets/i18n/de.yaml index d85f73589..96fd2f7f8 100644 --- a/marketplace-ui/src/assets/i18n/de.yaml +++ b/marketplace-ui/src/assets/i18n/de.yaml @@ -29,6 +29,10 @@ common: popularity: Beliebtheit alphabetically: Alphabetisch recent: Datum der Veröffentlichung + newest: Neuestes + oldest: Älteste + highest: Höchste + lowest: Niedrigste search: placeholder: Suche nothingFound: Nichts gefunden @@ -43,6 +47,31 @@ common: termsOfService: Nutzungsbedingungen product: detail: + backToMainPage: Zurück zur Marktplatzübersicht + review: Überprüfung + installation: Downloads + times: Zeiten + type: Typ + description: Beschreibung + installationGuide: Installationsanleitung + demo: Demo + maven: + label: Maven + description: Sie können es als Maven-Artefakt in Ihre pom.xml einbinden. + information: + label: Informationen + value: + author: Autor/Unterstützung + version: Version + compatibility: Kompatibilität + cost: Kosten + language: Sprache + industry: Branche + tag: Tags + source: Quelle + status: Status + moreInformation: Mehr Informationen + contactUs: Kontakt install: buttonLabel: Jetzt installieren download: @@ -52,4 +81,23 @@ common: versionSelector: label: Zielplattform artifactSelector: - label: Artefakt \ No newline at end of file + label: Artefakt + feedback: + label: Rückmeldung + successMessage: Ihr Feedback wurde erfolgreich übermittelt! + thankMessage: Vielen Dank für Ihren Beitrag + addFeedbackTitle: Geben Sie Ihr Feedback! + addFeedbackDescription: Durch Ihr Feedback ermöglichen Sie uns, unsere Produkte ständig zu verbessern und Ihnen und Ihrem Team stets die beste Qualität zu liefern. + currentProductTitle: Derzeitige Überprüfung + starRatingTitle: Sterne Bewertung + commentLabel: Einen Kommentar abgeben + submitBtnLabel: Bewertung abgeben + loggedGithubAsLabel: Eingeloggt bei Github als + reviewLabel: Überprüfung + detailedReviews: Ausführliche Bewertungen + rateLinkLabel: Diesen Stecker bewerten + showMoreBtnLabel: Mehr anzeigen + noFeedbackMessage1: Es gibt noch keine Rückmeldungen für diesen Anschluss. + noFeedbackMessage2: Seien Sie der Erste, der seine Meinung mitteilt. + rateFeedbackBtnLabel: Diesen Stecker bewerten + reviewLabelNoYet: Noch keine Bewertungen \ No newline at end of file diff --git a/marketplace-ui/src/assets/i18n/en.yaml b/marketplace-ui/src/assets/i18n/en.yaml index db1da4ff6..6b4d76a25 100644 --- a/marketplace-ui/src/assets/i18n/en.yaml +++ b/marketplace-ui/src/assets/i18n/en.yaml @@ -33,6 +33,10 @@ common: popularity: Popularity alphabetically: Alphabetically recent: Recent + newest: Newest + oldest: Oldest + highest: Highest + lowest: Lowest search: placeholder: Search a keyword nothingFound: Nothing found @@ -42,11 +46,36 @@ common: newVersionsInfo: New version release available for Linux,
Window and MacOS operating systems. downloadLatestLTSVersion: Download V10.0.18 downloadLatestDevVersion: Download V11.2.1 - ivyCompanyInfo: © 2023 Axon Ivy Inc + ivyCompanyInfo: © 2023 Axon Ivy Inc. privacyPolicy: Privacy Policy termsOfService: Terms of Service product: detail: + backToMainPage: Back to marketplace overview + review: Review + installation: Downloads + times: Times + type: Type + description: Description + installationGuide: Installation Guide + demo: Demo + maven: + label: Maven + description: You can consume it as maven artifact in your pom.xml + information: + label: Information + value: + author: Author/Support + version: Version + compatibility: Compatibility + cost: Cost + language: Language + industry: Industry + tag: Tags + source: Source + status: Status + moreInformation: More Information + contactUs: Contact Us install: buttonLabel: Install Now download: @@ -57,3 +86,22 @@ common: label: Choose target platform artifactSelector: label: Choose artifact + feedback: + label: Feedback + successMessage: Your feedback has been successfully submitted! + thankMessage: Thank you for your contribution + addFeedbackTitle: Give your feedback! + addFeedbackDescription: By giving your feedback you are enabling us to constantly improve our products and always deliver the best quality to both you and your team. + currentProductTitle: Currently reviewing + starRatingTitle: Stars rating + commentLabel: Let a comment + submitBtnLabel: Submit review + loggedGithubAsLabel: Logged in Github as + reviewLabel: Review + detailedReviews: Detailed reviews + rateLinkLabel: Rate this connector + showMoreBtnLabel: Show more + noFeedbackMessage1: There are no feedbacks for this connector yet. + noFeedbackMessage2: Bet the first to share your opinion. + rateFeedbackBtnLabel: Rate this connector + reviewLabelNoYet: No reviews yet diff --git a/marketplace-ui/src/assets/images/misc/avatar-default.png b/marketplace-ui/src/assets/images/misc/avatar-default.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c023c018ef54f46be1cbfd3f959d90a7598673 GIT binary patch literal 14350 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 literal 0 HcmV?d00001 diff --git a/marketplace-ui/src/assets/images/misc/confetti-icon.png b/marketplace-ui/src/assets/images/misc/confetti-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c034dcb9b2f239609dc74365ee61f727e7ff6481 GIT binary patch literal 40988 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 literal 0 HcmV?d00001 diff --git a/marketplace-ui/src/assets/images/misc/message-star.svg b/marketplace-ui/src/assets/images/misc/message-star.svg new file mode 100644 index 000000000..5053368c0 --- /dev/null +++ b/marketplace-ui/src/assets/images/misc/message-star.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/marketplace-ui/src/assets/scss/custom-modal-style.scss b/marketplace-ui/src/assets/scss/custom-modal-style.scss new file mode 100644 index 000000000..99b066f86 --- /dev/null +++ b/marketplace-ui/src/assets/scss/custom-modal-style.scss @@ -0,0 +1,44 @@ +.modal-header { + padding: 18px; +} + +.modal-body { + padding: 0; + scrollbar-width: none; +} + +.modal-content-wrapper { + display: flex; + flex-direction: column; + position: absolute; + height: 100%; + width: 100%; +} + +@media (min-width: 768px) { + .add-feedback-modal-dialog { + max-width: 627px; + + .modal-content { + border-radius: 10px; + height: 640px; + border: 0; + box-shadow: 0px 0px 26px 5px rgba(0, 0, 0, 0.25); + } + } +} + +.overflow-hidden { + overflow: hidden; +} + +.show-feedbacks-modal-dialog { + max-width: 884px; + + .modal-content { + border-radius: 10px; + height: 870px; + border: 0; + box-shadow: 0px 0px 26px 5px rgba(0, 0, 0, 0.25); + } +} \ No newline at end of file diff --git a/marketplace-ui/src/assets/scss/custom-style.scss b/marketplace-ui/src/assets/scss/custom-style.scss index 6dea467fa..353b3313e 100644 --- a/marketplace-ui/src/assets/scss/custom-style.scss +++ b/marketplace-ui/src/assets/scss/custom-style.scss @@ -1,9 +1,8 @@ // Required -@import '../../../node_modules/bootstrap/scss/functions'; +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; +@import 'bootstrap/scss/mixins'; -// Required -@import '../../../node_modules/bootstrap/scss/variables'; -@import '../../../node_modules/bootstrap/scss/mixins'; @font-face { font-family: Inter; src: url(../fonts/inter.ttf) format('truetype'); @@ -32,6 +31,11 @@ h1 { font-size: 72px; } +h2 { + font-weight: 600; + font-size: 64px; +} + h3 { font-weight: 600; font-size: 22px; @@ -42,6 +46,11 @@ h4 { font-size: 18px; } +h5 { + font-weight: 400; + font-size: 16px; +} + p { font-weight: 400; font-size: 14px; @@ -56,8 +65,19 @@ p { --ivy-text-primary-color: $ivyPrimaryTextColorLight; --ivy-text-secondary-color: $ivySecondaryTextLight; --ivy-border-color: #{$ivySecondaryButtonHoverLight}; + --ivy-textarea-background-color: #FAFAFA; --header-border-color: #ebebeb; + --footer-border-color: #e7e7e7; --ivy-secondary-border-color: #e7e7e7; + --ivy-custom-select-indicator: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 11 8 5 14 11'/%3e%3c/svg%3e") !important; + --active-tab-indicator-color: #{$ivyPrimaryTextColorLight}; + --info-dropdown-bg: #ffffff; + --info-dropdown-border: #0000001a; + + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 11 8 5 14 11'/%3e%3c/svg%3e") !important; + --star-color: #b0b0b0; + --star-filled-color: #{$ivyPrimaryTextColorLight}; + --text-no-rating-color: #757575; .bg-primary { background-color: #{$ivyPrimaryColorLight} !important; @@ -106,6 +126,10 @@ p { color: #{$ivyPrimaryTextColorLight} !important; } + .text-link-blue { + color: #{$ivyPrimaryColorLight} !important; + } + .border-dark { border-color: #{$ivyPrimaryTextColorLight}; } @@ -113,6 +137,16 @@ p { .border-primary { border-color: #{$ivyPrimaryTextColorLight} !important; } + + /** Star Rating Summary **/ + + .star-rating-counting-line { + background-color: #{$ivySecondaryColorLight} !important; + .star-rating-percent { + background-color: #{$ivyPrimaryColorLight} !important; + } + } + /** End Star Rating Summary **/ } .spinner-border { @@ -134,8 +168,17 @@ p { --ivy-text-secondary-color: #{$ivySecondaryTextDark}; --ivy-border-color: #{$ivySecondaryTextLight}; + --ivy-textarea-background-color: #333333; --header-border-color: #{$ivySecondaryTextLight}; + --footer-border-color: #4f4e4e; --ivy-secondary-border-color: #4f4e4e; + --ivy-custom-select-indicator: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 11 8 5 14 11'/%3e%3c/svg%3e") !important; + --active-tab-indicator-color: #{$white}; + --info-dropdown-bg: #{$ivyBodyBackgroundDark}; + --info-dropdown-border: #000000b2; + --star-color: #757575; + --star-filled-color: #{$white}; + --text-no-rating-color: #A3A3A3; a { color: #{$white}; @@ -145,6 +188,14 @@ p { background-color: #{$ivyNormalTextColorLight} !important; } + .bg-dark { + color: var(--bs-body-bg); + } + + .bg-white { + color: #{$white} !important; + } + .btn { border-radius: 0.5rem; } @@ -186,6 +237,16 @@ p { .border-light { border-color: #{$white} !important; } + + /** Star Rating Summary **/ + + .star-rating-counting-line { + background-color: #383838 !important; + .star-rating-percent { + background-color: #FAFAFA !important; + } + } + /** End Star Rating Summary **/ } *:focus { @@ -209,6 +270,18 @@ p { color: #858585 !important; } +@media (max-width: 568px) { + code { + font-size: 0.7rem; + } +} + +@media (max-width: 424px) { + code { + font-size: 0.57rem; + } +} + @media (min-width: 1440px) { .container { min-width: 1120px; diff --git a/marketplace-ui/src/environments/environment.development.ts b/marketplace-ui/src/environments/environment.development.ts index d77f76d7b..4a93e0eca 100644 --- a/marketplace-ui/src/environments/environment.development.ts +++ b/marketplace-ui/src/environments/environment.development.ts @@ -1,4 +1,7 @@ export const environment = { production: false, - apiUrl: 'http://localhost:9090/marketplace-service' + apiUrl: 'http://localhost:9090/marketplace-service', + githubClientId: 'Ov23liUzb36JCQIfEBGn', + githubAuthCallbackUrl: 'http://localhost:4200/auth/github/callback', + dayInMiliseconds: 86400000 }; diff --git a/marketplace-ui/src/environments/environment.ts b/marketplace-ui/src/environments/environment.ts index b0c3ad6e3..ed5e76440 100644 --- a/marketplace-ui/src/environments/environment.ts +++ b/marketplace-ui/src/environments/environment.ts @@ -1,4 +1,7 @@ export const environment = { production: true, - apiUrl: 'http://10.193.8.78:9090/marketplace-service' + apiUrl: 'http://10.193.8.78:9090/marketplace-service', + githubClientId: 'Ov23liVMliBxBqdQ7FnG', + githubAuthCallbackUrl: 'http://10.193.8.78:4200/auth/github/callback', + dayInMiliseconds: 86400000 }; diff --git a/marketplace-ui/src/styles.scss b/marketplace-ui/src/styles.scss index 27ca1cce6..c547a1ad4 100644 --- a/marketplace-ui/src/styles.scss +++ b/marketplace-ui/src/styles.scss @@ -1,9 +1,15 @@ -@import 'bootstrap'; -@import 'bootstrap-icons'; + +@import 'bootstrap/scss/bootstrap'; +@import 'bootstrap-icons/font/bootstrap-icons.css'; @import './assets/scss/custom-style.scss'; +@import './assets/scss/custom-modal-style.scss'; * { margin: 0; padding: 0; font-family: 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif; } + +body { + min-width: 300px; +} \ No newline at end of file From 445a3aa2ea3c6b36f0819563cce21bfb920151aa Mon Sep 17 00:00:00 2001 From: Khanh Nguyen <119989010+ndkhanh-axonivy@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:29:50 +0700 Subject: [PATCH 24/62] Feature/MARP-357 fix sonar (#39) --- marketplace-service/README.md | 17 +- marketplace-service/pom.xml | 6 +- .../market/MarketplaceServiceApplication.java | 2 +- .../assembler/FeedbackModelAssembler.java | 6 +- .../ProductDetailModelAssembler.java | 6 +- .../ArchivedArtifactsComparator.java | 10 +- .../comparator/LatestVersionComparator.java | 46 ++--- .../config/MarketApiDocumentConfig.java | 10 +- .../axonivy/market/config/MongoConfig.java | 70 ++++---- .../com/axonivy/market/config/WebConfig.java | 9 +- .../market/constants/EntityConstants.java | 10 +- .../market/constants/MavenConstants.java | 3 +- .../NonStandardProductPackageConstants.java | 3 +- .../constants/ProductJsonConstants.java | 3 +- .../market/controller/AppController.java | 3 +- .../market/controller/FeedbackController.java | 29 ++-- .../market/controller/OAuth2Controller.java | 9 +- .../market/controller/ProductController.java | 15 +- .../controller/ProductDetailsController.java | 18 +- .../com/axonivy/market/entity/Feedback.java | 13 -- .../market/entity/MavenArtifactModel.java | 38 ++--- .../market/entity/MavenArtifactVersion.java | 12 +- .../com/axonivy/market/entity/Product.java | 21 ++- .../java/com/axonivy/market/entity/User.java | 40 ++--- .../com/axonivy/market/enums/ErrorCode.java | 15 +- .../com/axonivy/market/enums/SortOption.java | 5 +- .../com/axonivy/market/enums/TypeOption.java | 3 +- .../market/exceptions/ExceptionHandlers.java | 6 +- .../model/Oauth2ExchangeCodeException.java | 8 +- .../market/factory/ProductFactory.java | 31 ++-- .../market/github/model/ArchivedArtifact.java | 8 +- .../market/github/model/MavenArtifact.java | 20 +-- .../service/GHAxonIvyMarketRepoService.java | 8 +- .../service/GHAxonIvyProductRepoService.java | 1 - .../market/github/service/GitHubService.java | 4 +- .../impl/GHAxonIvyMarketRepoServiceImpl.java | 9 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 32 ++-- .../service/impl/GitHubServiceImpl.java | 108 +++++++----- .../market/github/util/GitHubUtils.java | 129 +++++++------- .../axonivy/market/model/DisplayValue.java | 39 +++-- .../axonivy/market/model/FeedbackModel.java | 53 +++--- .../model/GitHubAccessTokenResponse.java | 22 +++ .../model/MavenArtifactVersionModel.java | 4 +- .../market/model/Oauth2AuthorizationCode.java | 2 +- .../axonivy/market/model/ProductModel.java | 34 ++-- .../market/repository/UserRepository.java | 2 +- .../market/schedulingtask/ScheduledTasks.java | 2 +- .../market/service/FeedbackService.java | 7 +- .../axonivy/market/service/JwtService.java | 2 + .../axonivy/market/service/UserService.java | 2 + .../market/service/VersionService.java | 10 +- .../service/impl/FeedbackServiceImpl.java | 58 ++++--- .../market/service/impl/JwtServiceImpl.java | 8 +- .../service/impl/ProductServiceImpl.java | 158 +++++++++--------- .../market/service/impl/UserServiceImpl.java | 3 +- .../service/impl/VersionServiceImpl.java | 53 +++--- .../axonivy/market/util/XmlReaderUtils.java | 3 +- .../controller/FeedbackControllerTest.java | 14 +- .../controller/OAuth2ControllerTest.java | 9 +- .../controller/ProductControllerTest.java | 9 - .../ProductDetailsControllerTest.java | 47 +++--- .../market/factory/ProductFactoryTest.java | 26 ++- .../service/FeedbackServiceImplTest.java | 65 ++++--- .../GHAxonIvyMarketRepoServiceImplTest.java | 6 +- .../GHAxonIvyProductRepoServiceImplTest.java | 44 +++-- .../market/service/GitHubServiceImplTest.java | 4 +- .../market/service/JwtServiceImplTest.java | 153 ++++++++--------- .../service/ProductServiceImplTest.java | 118 +++++++------ .../market/service/SchedulingTasksTest.java | 2 +- .../service/VersionServiceImplTest.java | 102 +++++------ .../src/test/resources/meta.json | 8 +- 71 files changed, 989 insertions(+), 866 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/GitHubAccessTokenResponse.java diff --git a/marketplace-service/README.md b/marketplace-service/README.md index 4d546f33a..d85b7f861 100644 --- a/marketplace-service/README.md +++ b/marketplace-service/README.md @@ -1,6 +1,7 @@ # Getting Started ### Reference Documentation + For further reference, please consider the following sections: * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) @@ -9,9 +10,11 @@ For further reference, please consider the following sections: * [Spring Web](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#web) ### Guides + The following guides illustrate how to use some features concretely: -* Installing mongodb, and access it as Url mongodb://localhost:27017/, and you can create and name whatever you want ,then you should put them to application.properties +* Installing mongodb, and access it as Url mongodb://localhost:27017/, and you can create and name whatever you want + ,then you should put them to application.properties * You can change the MongoDB configuration in file `application.properties` ``` spring.data.mongodb.host= @@ -21,14 +24,18 @@ The following guides illustrate how to use some features concretely: * Run mvn clean install to build project * Run mvn test to test all tests - ### Access Swagger URL: http://{your-host}/swagger-ui/index.html ### Install Lombok for Eclipse IDE + * Download lombok here https://projectlombok.org/download -* run command "java -jar lombok.jar" then you can access file “eclipse.ini“ in eclipse folder where you install → there is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are successful +* run command "java -jar lombok.jar" then you can access file “eclipse.ini“ in eclipse folder where you install → there + is a text like this: -javaagent:C:\Users\tvtphuc\eclipse\jee-2024-032\eclipse\lombok.jar → it means you are + successful * Start eclipse * Import the project then in the eclipse , you should run the command “mvn clean install“ -* After that you go to class MarketplaceServiceApplication → right click to main method → click run as → choose Java Application +* After that you go to class MarketplaceServiceApplication → right click to main method → click run as → choose Java + Application * Then you can send a request in postman -* If you want to run single test in class UserServiceImplTest. You can right-click to method testFindAllUser and right click → select Run as → choose JUnit Test \ No newline at end of file +* If you want to run single test in class UserServiceImplTest. You can right-click to method testFindAllUser and right + click → select Run as → choose JUnit Test \ No newline at end of file diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml index d6a0d9a67..44d55027e 100644 --- a/marketplace-service/pom.xml +++ b/marketplace-service/pom.xml @@ -1,13 +1,13 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.5 - + com.axonivy.market marketplace-service diff --git a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java index 52cdb27d9..74c7b12d4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java +++ b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -17,7 +17,7 @@ @SpringBootApplication public class MarketplaceServiceApplication { - private ProductService productService; + private final ProductService productService; public MarketplaceServiceApplication(ProductService productService) { this.productService = productService; diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java index a981b099a..154abbe99 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java @@ -28,8 +28,7 @@ public FeedbackModelAssembler(UserService userService) { @Override public FeedbackModel toModel(Feedback feedback) { FeedbackModel resource = new FeedbackModel(); - resource.add(linkTo(methodOn(FeedbackController.class).findFeedback(feedback.getId())) - .withSelfRel()); + resource.add(linkTo(methodOn(FeedbackController.class).findFeedback(feedback.getId())).withSelfRel()); return createResource(resource, feedback); } @@ -37,8 +36,7 @@ private FeedbackModel createResource(FeedbackModel model, Feedback feedback) { User user; try { user = userService.findUser(feedback.getUserId()); - } - catch (NotFoundException e) { + } catch (NotFoundException e) { log.warn(e.getMessage()); user = new User(); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index ff9ade568..e72ab8034 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -1,8 +1,5 @@ package com.axonivy.market.assembler; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; @@ -14,6 +11,9 @@ import java.util.List; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + @Component public class ProductDetailModelAssembler extends RepresentationModelAssemblerSupport { diff --git a/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java index 7a27d7718..d5c553ed2 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java +++ b/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java @@ -5,10 +5,10 @@ import java.util.Comparator; public class ArchivedArtifactsComparator implements Comparator { - private final LatestVersionComparator comparator = new LatestVersionComparator(); + private final LatestVersionComparator comparator = new LatestVersionComparator(); - @Override - public int compare(ArchivedArtifact artifact1, ArchivedArtifact artifact2) { - return comparator.compare(artifact1.getLastVersion(), artifact2.getLastVersion()); - } + @Override + public int compare(ArchivedArtifact artifact1, ArchivedArtifact artifact2) { + return comparator.compare(artifact1.getLastVersion(), artifact2.getLastVersion()); + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java index 9419b411f..17d90e397 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java +++ b/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java @@ -4,29 +4,29 @@ public class LatestVersionComparator implements Comparator { - @Override - public int compare(String v1, String v2) { - // Split by "." - String[] parts1 = v1.split("\\."); - String[] parts2 = v2.split("\\."); + @Override + public int compare(String v1, String v2) { + // Split by "." + String[] parts1 = v1.split("\\."); + String[] parts2 = v2.split("\\."); - // Compare up to the shorter length - int length = Math.min(parts1.length, parts2.length); - for (int i = 0; i < length; i++) { - try { - int num1 = Integer.parseInt(parts1[i]); - int num2 = Integer.parseInt(parts2[i]); - // Return difference for numeric parts - if (num1 != num2) { - return num2 - num1; - } - // Handle non-numeric parts (e.g., "m229") - } catch (NumberFormatException e) { - return parts2[i].replaceAll("\\D", "").compareTo(parts1[i].replaceAll("\\D", "")); - } - } + // Compare up to the shorter length + int length = Math.min(parts1.length, parts2.length); + for (int i = 0; i < length; i++) { + try { + int num1 = Integer.parseInt(parts1[i]); + int num2 = Integer.parseInt(parts2[i]); + // Return difference for numeric parts + if (num1 != num2) { + return num2 - num1; + } + // Handle non-numeric parts (e.g., "m229") + } catch (NumberFormatException e) { + return parts2[i].replaceAll("\\D", "").compareTo(parts1[i].replaceAll("\\D", "")); + } + } - // Versions with more parts are considered larger - return parts2.length - parts1.length; - } + // Versions with more parts are considered larger + return parts2.length - parts1.length; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java index 805f30120..0fb1dc852 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -20,18 +20,14 @@ public class MarketApiDocumentConfig { @Bean public GroupedOpenApi buildMarketCustomHeader() { - return GroupedOpenApi.builder() - .group(DEFAULT_DOC_GROUP) - .addOpenApiCustomizer(customMarketHeaders()) - .pathsToMatch(PATH_PATTERN) - .build(); + return GroupedOpenApi.builder().group(DEFAULT_DOC_GROUP).addOpenApiCustomizer(customMarketHeaders()) + .pathsToMatch(PATH_PATTERN).build(); } private OpenApiCustomizer customMarketHeaders() { return openApi -> openApi.getPaths().values().forEach((PathItem pathItem) -> { for (Operation operation : pathItem.readOperations()) { - Parameter headerParameter = new Parameter().in(HEADER_PARAM) - .schema(new StringSchema()).name(REQUESTED_BY) + Parameter headerParameter = new Parameter().in(HEADER_PARAM).schema(new StringSchema()).name(REQUESTED_BY) .description(DEFAULT_PARAM).required(true); operation.addParametersItem(headerParameter); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java index 7f558f1cc..315b7384c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -22,39 +22,39 @@ @EnableMongoAuditing public class MongoConfig extends AbstractMongoClientConfiguration { - @Value("${spring.data.mongodb.host}") - private String host; - - @Value("${spring.data.mongodb.database}") - private String databaseName; - - @Override - protected String getDatabaseName() { - return databaseName; - } - - @Override - public MongoClient mongoClient() { - ConnectionString connectionString = new ConnectionString(host); - MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString) - .build(); - - return MongoClients.create(mongoClientSettings); - } - - /** - * By default, the key in hash map is not allow to contain dot character (.) we - * need to escape it by define a replacement to that char - **/ - @Override - @Bean - public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, - MongoCustomConversions customConversions, MongoMappingContext mappingContext) { - DbRefResolver dbRefResolver = new DefaultDbRefResolver(databaseFactory); - MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext); - converter.setCustomConversions(customConversions); - converter.setCodecRegistryProvider(databaseFactory); - converter.setMapKeyDotReplacement("_"); - return converter; - } + @Value("${spring.data.mongodb.host}") + private String host; + + @Value("${spring.data.mongodb.database}") + private String databaseName; + + @Override + protected String getDatabaseName() { + return databaseName; + } + + @Override + public MongoClient mongoClient() { + ConnectionString connectionString = new ConnectionString(host); + MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString) + .build(); + + return MongoClients.create(mongoClientSettings); + } + + /** + * By default, the key in hash map is not allow to contain dot character (.) we need to escape it by define a + * replacement to that char + **/ + @Override + @Bean + public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, + MongoCustomConversions customConversions, MongoMappingContext mappingContext) { + DbRefResolver dbRefResolver = new DefaultDbRefResolver(databaseFactory); + MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext); + converter.setCustomConversions(customConversions); + converter.setCodecRegistryProvider(databaseFactory); + converter.setMapKeyDotReplacement("_"); + return converter; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java index b9d75afa3..a781aa8ea 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java @@ -11,7 +11,7 @@ public class WebConfig implements WebMvcConfigurer { private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**" }; private static final String[] ALLOWED_HEADERS = { "Accept-Language", "Content-Type", "Authorization", - "X-Requested-By", "x-requested-with", "X-Forwarded-Host" }; + "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token" }; private static final String[] ALLOWED_METHODS = { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; private final MarketHeaderInterceptor headerInterceptor; @@ -33,10 +33,7 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOriginPatterns(marketCorsAllowedOriginPatterns) - .allowedMethods(ALLOWED_METHODS) - .allowedHeaders(ALLOWED_HEADERS) - .maxAge(marketCorsAllowedOriginMaxAge); + registry.addMapping("/**").allowedOriginPatterns(marketCorsAllowedOriginPatterns).allowedMethods(ALLOWED_METHODS) + .allowedHeaders(ALLOWED_HEADERS).maxAge(marketCorsAllowedOriginMaxAge); } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java index 0d9752cb9..d6bd38fbd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -5,9 +5,9 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class EntityConstants { - public static final String USER = "User"; - public static final String PRODUCT = "Product"; - public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; - public static final String GH_REPO_META = "GitHubRepoMeta"; - public static final String FEEDBACK = "Feedback"; + public static final String USER = "User"; + public static final String PRODUCT = "Product"; + public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; + public static final String GH_REPO_META = "GitHubRepoMeta"; + public static final String FEEDBACK = "Feedback"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java index 4ba05471d..71cdb72ee 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java @@ -1,7 +1,8 @@ package com.axonivy.market.constants; public class MavenConstants { - private MavenConstants() {} + private MavenConstants() { + } public static final String SNAPSHOT_RELEASE_POSTFIX = "-SNAPSHOT"; public static final String SPRINT_RELEASE_POSTFIX = "-m"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java index 133ff55ff..dec20303d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java @@ -1,7 +1,8 @@ package com.axonivy.market.constants; public class NonStandardProductPackageConstants { - private NonStandardProductPackageConstants() {} + private NonStandardProductPackageConstants() { + } public static final String PORTAL = "portal"; public static final String MICROSOFT_REPO_NAME = "msgraph-connector"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java index 96c6eb5e1..8d3aa03a2 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java @@ -17,5 +17,6 @@ public class ProductJsonConstants { public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; - private ProductJsonConstants() {} + private ProductJsonConstants() { + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java index 45f712d2d..536b348cf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java @@ -23,8 +23,7 @@ public ResponseEntity root() { var message = new Message(); message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); message.setMessageDetails( - "Marketplace API is a REST APIs for Marketplace website. Try with %s" - .formatted(extractSwaggerUrl())); + "Marketplace API is a REST APIs for Marketplace website. Try with %s".formatted(extractSwaggerUrl())); message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); return new ResponseEntity<>(message, HttpStatus.OK); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java index 233c94b4e..4d04faa3c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -16,7 +16,14 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +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.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; @@ -34,7 +41,8 @@ public class FeedbackController { private final PagedResourcesAssembler pagedResourcesAssembler; - public FeedbackController(FeedbackService feedbackService, JwtService jwtService, FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { + public FeedbackController(FeedbackService feedbackService, JwtService jwtService, + FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { this.feedbackService = feedbackService; this.jwtService = jwtService; this.feedbackModelAssembler = feedbackModelAssembler; @@ -43,7 +51,8 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService @Operation(summary = "Find all feedbacks by product id") @GetMapping("/product/{productId}") - public ResponseEntity> findFeedbacks(@PathVariable("productId") String productId, Pageable pageable) { + public ResponseEntity> findFeedbacks(@PathVariable("productId") String productId, + Pageable pageable) { Page results = feedbackService.findFeedbacks(productId, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); @@ -61,15 +70,15 @@ public ResponseEntity findFeedback(@PathVariable("id") String id) @Operation(summary = "Find all feedbacks by user id and product id") @GetMapping() - public ResponseEntity findFeedbackByUserIdAndProductId( - @RequestParam String userId, + public ResponseEntity findFeedbackByUserIdAndProductId(@RequestParam String userId, @RequestParam String productId) { Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); } @PostMapping - public ResponseEntity createFeedback(@RequestBody @Valid Feedback feedback, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { + public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModel feedback, + @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { String token = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { token = authorizationHeader.substring(7); // Remove "Bearer " prefix @@ -84,9 +93,7 @@ public ResponseEntity createFeedback(@RequestBody @Valid Feedback feedback feedback.setUserId(claims.getSubject()); Feedback newFeedback = feedbackService.upsertFeedback(feedback); - URI location = ServletUriComponentsBuilder.fromCurrentRequest() - .path("/{id}") - .buildAndExpand(newFeedback.getId()) + URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(newFeedback.getId()) .toUri(); return ResponseEntity.created(location).build(); @@ -100,8 +107,8 @@ public ResponseEntity> getProductRating(@PathVariable("produ @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { - var emptyPagedModel = (PagedModel) pagedResourcesAssembler - .toEmptyModel(Page.empty(), FeedbackModel.class); + var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), + FeedbackModel.class); return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java index a3ebbca64..611b6a860 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -3,6 +3,7 @@ import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.User; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.GitHubAccessTokenResponse; import com.axonivy.market.model.Oauth2AuthorizationCode; import com.axonivy.market.service.JwtService; import org.springframework.beans.factory.annotation.Value; @@ -13,7 +14,6 @@ import org.springframework.web.bind.annotation.RestController; import java.util.Collections; -import java.util.Map; @RestController @RequestMapping("/auth") @@ -35,9 +35,10 @@ public OAuth2Controller(GitHubService gitHubService, JwtService jwtService) { } @PostMapping("/github/login") - public ResponseEntity gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { - Map tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, clientSecret); - String accessToken = (String) tokenResponse.get(GitHubConstants.Json.ACCESS_TOKEN); + public ResponseEntity gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { + GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, + clientSecret); + String accessToken = tokenResponse.getAccessToken(); User user = gitHubService.getAndUpdateUser(accessToken); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index 6dbd73c4d..498424e11 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -15,7 +15,11 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; import static com.axonivy.market.constants.RequestMappingConstants.SYNC; @@ -29,7 +33,7 @@ public class ProductController { private final PagedResourcesAssembler pagedResourcesAssembler; public ProductController(ProductService productService, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { + PagedResourcesAssembler pagedResourcesAssembler) { this.productService = productService; this.assembler = assembler; this.pagedResourcesAssembler = pagedResourcesAssembler; @@ -37,8 +41,7 @@ public ProductController(ProductService productService, ProductModelAssembler as @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() - public ResponseEntity> findProducts( - @RequestParam(name = "type") String type, + public ResponseEntity> findProducts(@RequestParam(name = "type") String type, @RequestParam(required = false, name = "keyword") String keyword, @RequestParam(name = "language") String language, Pageable pageable) { Page results = productService.findProducts(type, keyword, language, pageable); @@ -69,8 +72,8 @@ public ResponseEntity syncProducts() { @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { - var emptyPagedModel = - (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), ProductModel.class); + var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), + ProductModel.class); return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index 87fa6b218..b57f80e0f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -1,9 +1,11 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; - -import java.util.List; - +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -13,13 +15,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.axonivy.market.assembler.ProductDetailModelAssembler; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.service.ProductService; -import com.axonivy.market.service.VersionService; +import java.util.List; -import io.swagger.v3.oas.annotations.Operation; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; @RestController @RequestMapping(PRODUCT_DETAILS) diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java index 166da0c23..fea74af1b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java @@ -1,9 +1,5 @@ package com.axonivy.market.entity; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -29,18 +25,9 @@ public class Feedback implements Serializable { @Id private String id; - private String userId; - - @NotBlank(message = "Product id cannot be blank") private String productId; - - @NotBlank(message = "Content cannot be blank") - @Size(max = 5, message = "Content length must be up to 250 characters") private String content; - - @Min(value = 1, message = "Rating should not be less than 1") - @Max(value = 5, message = "Rating should not be greater than 5") private Integer rating; @CreatedDate diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java index 2d48d4c6a..1c9ad87ec 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java @@ -14,25 +14,25 @@ @Setter @Getter public class MavenArtifactModel implements Serializable { - private static final long serialVersionUID = 1L; - private String name; - private String downloadUrl; - @Transient - private Boolean isProductArtifact; + private static final long serialVersionUID = 1L; + private String name; + private String downloadUrl; + @Transient + private Boolean isProductArtifact; - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) { - return false; - } - MavenArtifactModel reference = (MavenArtifactModel) object; - return Objects.equals(name, reference.getName()) && Objects.equals(downloadUrl, reference.getDownloadUrl()); - } + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) { + return false; + } + MavenArtifactModel reference = (MavenArtifactModel) object; + return Objects.equals(name, reference.getName()) && Objects.equals(downloadUrl, reference.getDownloadUrl()); + } - @Override - public int hashCode() { - return Objects.hash(name, downloadUrl); - } + @Override + public int hashCode() { + return Objects.hash(name, downloadUrl); + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java index 8c8820579..8f2e87291 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java @@ -24,11 +24,11 @@ public class MavenArtifactVersion implements Serializable { private static final long serialVersionUID = -6492612804634492078L; @Id - private String productId; - private List versions = new ArrayList<>(); - private Map> productArtifactWithVersionReleased = new HashMap<>(); + private String productId; + private List versions = new ArrayList<>(); + private Map> productArtifactWithVersionReleased = new HashMap<>(); - public MavenArtifactVersion(String productId) { - this.productId = productId; - } + public MavenArtifactVersion(String productId) { + this.productId = productId; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index b1a135c31..a6d4c39df 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -1,21 +1,24 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.PRODUCT; - -import java.io.Serializable; -import java.util.Date; -import java.util.List; - - +import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; -import com.axonivy.market.github.model.MavenArtifact; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import static com.axonivy.market.constants.EntityConstants.PRODUCT; + @Getter @Setter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java index 0f8e7b612..65c996981 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java @@ -19,30 +19,30 @@ @NoArgsConstructor @Document(USER) public class User implements Serializable { - @Serial - private static final long serialVersionUID = -1244486023332931059L; + @Serial + private static final long serialVersionUID = -1244486023332931059L; - @Id - private String id; + @Id + private String id; - @Indexed(unique = true) - private String gitHubId; + @Indexed(unique = true) + private String gitHubId; - private String provider; - private String username; - private String name; - private String avatarUrl; + private String provider; + private String username; + private String name; + private String avatarUrl; - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((User) obj).getId()).isEquals(); + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; } + return new EqualsBuilder().append(id, ((User) obj).getId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index 7aef2b47c..650463ef5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -4,12 +4,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; - /** - * @fo {@link ErrorCode} is a presentation for a system code during proceeding - * data It has format cseo - 0000 c present for controller s present for - * service e present for entity o present for other And 0000 is a successful - * code + * @fo {@link ErrorCode} is a presentation for a system code during proceeding data It has format cseo - 0000 c present + * for controller s present for service e present for entity o present for other And 0000 is a successful code */ @Getter @@ -17,12 +14,10 @@ @NoArgsConstructor public enum ErrorCode { SUCCESSFUL("0000", "SUCCESSFUL"), PRODUCT_FILTER_INVALID("1101", "PRODUCT_FILTER_INVALID"), - PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"), - PRODUCT_NOT_FOUND("1103", "PRODUCT_NOT_FOUND"), + PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"), PRODUCT_NOT_FOUND("1103", "PRODUCT_NOT_FOUND"), GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), - GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), - USER_NOT_FOUND("2103", "USER_NOT_FOUND"), - FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), + GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), USER_NOT_FOUND("2103", "USER_NOT_FOUND"), + GITHUB_USER_NOT_FOUND("2204", "GITHUB_USER_NOT_FOUND"), FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); String code; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java index c3e9714e3..303c57b2b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java @@ -25,8 +25,7 @@ public static SortOption of(String option) { } public String getCode(String language) { - return StringUtils.isNotBlank(language) && ALPHABETICALLY.option.equalsIgnoreCase(option) - ? String.format("%s.%s", ALPHABETICALLY.code, language) - : code; + return StringUtils.isNotBlank(language) && ALPHABETICALLY.option.equalsIgnoreCase(option) ? String.format("%s.%s", + ALPHABETICALLY.code, language) : code; } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java index 1c30aca92..3176a7ba9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java @@ -6,7 +6,8 @@ @Getter public enum TypeOption { - ALL("all", ""), CONNECTORS("connectors", "connector"), UTILITIES("utilities", "util"), SOLUTIONS("solutions", "solution"), DEMOS("demos", "demo"); + ALL("all", ""), CONNECTORS("connectors", "connector"), UTILITIES("utilities", "util"), + SOLUTIONS("solutions", "solution"), DEMOS("demos", "demo"); private String option; private String code; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index d9b1ab725..948d28fc3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -25,7 +25,8 @@ public class ExceptionHandlers extends ResponseEntityExceptionHandler { @Override - protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, + HttpStatusCode status, WebRequest request) { BindingResult bindingResult = ex.getBindingResult(); List errors = new ArrayList<>(); if (bindingResult.hasErrors()) { @@ -66,7 +67,8 @@ public ResponseEntity handleInvalidException(InvalidParamException inval } @ExceptionHandler(Oauth2ExchangeCodeException.class) - public ResponseEntity handleOauth2ExchangeCodeException(Oauth2ExchangeCodeException oauth2ExchangeCodeException) { + public ResponseEntity handleOauth2ExchangeCodeException( + Oauth2ExchangeCodeException oauth2ExchangeCodeException) { var errorMessage = new Message(); errorMessage.setHelpCode(oauth2ExchangeCodeException.getError()); errorMessage.setMessageDetails(oauth2ExchangeCodeException.getErrorDescription()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java index d48a88770..09448e2bb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java @@ -11,9 +11,9 @@ @AllArgsConstructor public class Oauth2ExchangeCodeException extends RuntimeException { - @Serial - private static final long serialVersionUID = 6778659816121728814L; + @Serial + private static final long serialVersionUID = 6778659816121728814L; - private String error; - private String errorDescription; + private final String error; + private final String errorDescription; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index 38e16a438..b7c4580ec 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -1,31 +1,30 @@ package com.axonivy.market.factory; -import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.*; -import static org.apache.commons.lang3.StringUtils.EMPTY; - +import com.axonivy.market.entity.Product; import com.axonivy.market.enums.Language; +import com.axonivy.market.github.model.Meta; import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.DisplayValue; import com.axonivy.market.model.MultilingualismValue; -import org.apache.commons.lang3.BooleanUtils; - -import java.io.IOException; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.kohsuke.github.GHContent; - -import com.axonivy.market.entity.Product; -import com.axonivy.market.github.model.Meta; import com.fasterxml.jackson.databind.ObjectMapper; - import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.github.GHContent; import org.springframework.util.CollectionUtils; +import java.io.IOException; +import java.util.List; + +import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_NAME; +import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_URL; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.apache.commons.lang3.StringUtils.EMPTY; + @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ProductFactory { diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java index f9ff5a69f..0bd450349 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java @@ -16,8 +16,8 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) public class ArchivedArtifact implements Serializable { - private static final long serialVersionUID = 1L; - private String lastVersion; - private String groupId; - private String artifactId; + private static final long serialVersionUID = 1L; + private String lastVersion; + private String groupId; + private String artifactId; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java index 811b7917b..5c7cf3a37 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java @@ -18,14 +18,14 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) public class MavenArtifact implements Serializable { - private static final long serialVersionUID = 1L; - private String repoUrl; - private String name; - private String groupId; - private String artifactId; - private String type; - private Boolean isDependency; - @Transient - private Boolean isProductArtifact; - private List archivedArtifacts; + private static final long serialVersionUID = 1L; + private String repoUrl; + private String name; + private String groupId; + private String artifactId; + private String type; + private Boolean isDependency; + @Transient + private Boolean isProductArtifact; + private List archivedArtifacts; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java index 8a3cb88f3..a75787bd1 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java @@ -10,11 +10,11 @@ public interface GHAxonIvyMarketRepoService { - public Map> fetchAllMarketItems(); + Map> fetchAllMarketItems(); - public GHCommit getLastCommit(long lastCommitTime); + GHCommit getLastCommit(long lastCommitTime); - public List fetchMarketItemsBySHA1Range(String fromSHA1, String toSHA1); + List fetchMarketItemsBySHA1Range(String fromSHA1, String toSHA1); - public GHRepository getRepository(); + GHRepository getRepository(); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index 46afa0f0c..2f26df9c6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -3,7 +3,6 @@ import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.github.model.MavenArtifact; - import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index 5cf1a9d01..6e4a19ff5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -1,6 +1,7 @@ package com.axonivy.market.github.service; import com.axonivy.market.entity.User; +import com.axonivy.market.model.GitHubAccessTokenResponse; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; @@ -8,7 +9,6 @@ import java.io.IOException; import java.util.List; -import java.util.Map; public interface GitHubService { @@ -22,7 +22,7 @@ public interface GitHubService { GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; - Map getAccessToken(String code, String clientId, String clientSecret); + GitHubAccessTokenResponse getAccessToken(String code, String clientId, String clientSecret); User getAndUpdateUser(String accessToken); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index 61aca55a9..1d3f286eb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -8,7 +8,12 @@ import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; import lombok.extern.log4j.Log4j2; -import org.kohsuke.github.*; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHCommitQueryBuilder; +import org.kohsuke.github.GHCompare; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; import org.springframework.stereotype.Service; import java.io.IOException; @@ -64,7 +69,7 @@ private void extractFileInDirectoryContent(GHContent content, Map convertProductJsonToMavenProductInfo(GHContent conten JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); // Not convert to artifact if id of node is not maven-import or maven-dependency - List installerIdsToDisplay = - List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); + List installerIdsToDisplay = List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, + ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { continue; } @@ -200,7 +202,8 @@ public String updateImagesWithDownloadUrl(Product product, List conte } for (Map.Entry entry : imageUrls.entrySet()) { String imageUrlPattern = String.format(README_IMAGE_FORMAT, Pattern.quote(entry.getKey())); - readmeContents = readmeContents.replaceAll(imageUrlPattern, String.format(IMAGE_DOWNLOAD_URL_FORMAT,entry.getValue())); + readmeContents = readmeContents.replaceAll(imageUrlPattern, + String.format(IMAGE_DOWNLOAD_URL_FORMAT, entry.getValue())); } return readmeContents; @@ -255,8 +258,7 @@ private List getProductFolderContents(Product product, GHRepository g throws IOException { String productFolderPath = ghRepository.getDirectoryContent(CommonConstants.SLASH, tag).stream() .filter(GHContent::isDirectory).map(GHContent::getName) - .filter(content -> content.endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findFirst() - .orElse(null); + .filter(content -> content.endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findFirst().orElse(null); if (StringUtils.isBlank(productFolderPath) || hasChildConnector(ghRepository)) { productFolderPath = GitHubUtils.getNonStandardProductFilePath(product.getId()); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index 6af97d1fa..ba63635ad 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -2,12 +2,24 @@ import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.User; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.GitHubAccessTokenResponse; import com.axonivy.market.repository.UserRepository; -import org.kohsuke.github.*; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.*; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -64,56 +76,62 @@ public GHContent getGHContent(GHRepository ghRepository, String path, String ref return ghRepository.getFileContent(path, ref); } - @Override - public Map getAccessToken(String code, String clientId, String clientSecret) throws Oauth2ExchangeCodeException { - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add(GitHubConstants.Json.CLIENT_ID, clientId); - params.add(GitHubConstants.Json.CLIENT_SECRET, clientSecret); - params.add(GitHubConstants.Json.CODE, code); - - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - HttpEntity> request = new HttpEntity<>(params, headers); - - ResponseEntity response = restTemplate.postForEntity(GitHubConstants.GITHUB_GET_ACCESS_TOKEN_URL, request, Map.class); - if (response.getBody().containsKey(GitHubConstants.Json.ERROR)) { - throw new Oauth2ExchangeCodeException(response.getBody().get(GitHubConstants.Json.ERROR).toString(), response.getBody().get(GitHubConstants.Json.ERROR_DESCRIPTION).toString()); - } - return response.getBody(); + @Override + public GitHubAccessTokenResponse getAccessToken(String code, String clientId, String clientSecret) + throws Oauth2ExchangeCodeException { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(GitHubConstants.Json.CLIENT_ID, clientId); + params.add(GitHubConstants.Json.CLIENT_SECRET, clientSecret); + params.add(GitHubConstants.Json.CODE, code); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity responseEntity = restTemplate.postForEntity( + GitHubConstants.GITHUB_GET_ACCESS_TOKEN_URL, request, GitHubAccessTokenResponse.class); + GitHubAccessTokenResponse response = responseEntity.getBody(); + + if (response != null && response.getError() != null && !response.getError().isBlank()) { + throw new Oauth2ExchangeCodeException(response.getError(), response.getErrorDescription()); } - @Override - public User getAndUpdateUser(String accessToken) { - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - HttpEntity entity = new HttpEntity<>(headers); - - ResponseEntity response = restTemplate.exchange( - "https://api.github.com/user", HttpMethod.GET, entity, Map.class); + return response; + } - Map userDetails = response.getBody(); + @Override + public User getAndUpdateUser(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); - if (userDetails == null) { - throw new RuntimeException("Failed to fetch user details from GitHub"); - } + ResponseEntity> response = restTemplate.exchange("https://api.github.com/user", HttpMethod.GET, + entity, new ParameterizedTypeReference<>() { + }); - String gitHubId = userDetails.get(GitHubConstants.Json.USER_ID).toString(); - String name = (String) userDetails.get(GitHubConstants.Json.USER_NAME); - String avatarUrl = (String) userDetails.get(GitHubConstants.Json.USER_AVATAR_URL); - String username = (String) userDetails.get(GitHubConstants.Json.USER_LOGIN_NAME); + Map userDetails = response.getBody(); - User user = userRepository.searchByGitHubId(gitHubId); - if (user == null) { - user = new User(); - } - user.setGitHubId(gitHubId); - user.setName(name); - user.setUsername(username); - user.setAvatarUrl(avatarUrl); - user.setProvider(GitHubConstants.GITHUB_PROVIDER_NAME); + if (userDetails == null) { + throw new NotFoundException(ErrorCode.GITHUB_USER_NOT_FOUND, "Failed to fetch user details from GitHub"); + } - userRepository.save(user); + String gitHubId = userDetails.get(GitHubConstants.Json.USER_ID).toString(); + String name = (String) userDetails.get(GitHubConstants.Json.USER_NAME); + String avatarUrl = (String) userDetails.get(GitHubConstants.Json.USER_AVATAR_URL); + String username = (String) userDetails.get(GitHubConstants.Json.USER_LOGIN_NAME); - return user; + User user = userRepository.searchByGitHubId(gitHubId); + if (user == null) { + user = new User(); } + user.setGitHubId(gitHubId); + user.setName(name); + user.setUsername(username); + user.setAvatarUrl(avatarUrl); + user.setProvider(GitHubConstants.GITHUB_PROVIDER_NAME); + + userRepository.save(user); + + return user; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java index 4ccd7f1d4..c3b172014 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -1,20 +1,19 @@ package com.axonivy.market.github.util; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.NonStandardProductPackageConstants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.PagedIterable; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.extern.log4j.Log4j2; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -24,7 +23,7 @@ public class GitHubUtils { private static String pathToImageFolder; public static long getGHCommitDate(GHCommit commit) { - long commitTime = 0l; + long commitTime = 0L; if (commit != null) { try { commitTime = commit.getCommitDate().getTime(); @@ -66,68 +65,68 @@ public static String convertArtifactIdToName(String artifactId) { public static String getNonStandardProductFilePath(String productId) { switch (productId) { - case NonStandardProductPackageConstants.PORTAL: - pathToProductFolderFromTagContent = "AxonIvyPortal/portal-product"; - break; - case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: - pathToProductFolderFromTagContent = "connectivity/connectivity-demos-product"; - break; - case NonStandardProductPackageConstants.ERROR_HANDLING: - pathToProductFolderFromTagContent = "error-handling/error-handling-demos-product"; - break; - case NonStandardProductPackageConstants.WORKFLOW_DEMO: - pathToProductFolderFromTagContent = "workflow/workflow-demos-product"; - break; - case NonStandardProductPackageConstants.MICROSOFT_365: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-connector"; - break; - case NonStandardProductPackageConstants.MICROSOFT_CALENDAR: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-calendar"; - break; - case NonStandardProductPackageConstants.MICROSOFT_TEAMS: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-chat"; - break; - case NonStandardProductPackageConstants.MICROSOFT_MAIL: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-mail"; - break; - case NonStandardProductPackageConstants.MICROSOFT_TODO: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-todo"; - break; - case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: - pathToProductFolderFromTagContent = "html-dialog/html-dialog-demos-product"; - break; - case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: - pathToProductFolderFromTagContent = "rule-engine/rule-engine-demos-product"; - break; - case NonStandardProductPackageConstants.OPENAI_CONNECTOR: - pathToProductFolderFromTagContent = "openai-connector-product"; - break; - case NonStandardProductPackageConstants.OPENAI_ASSISTANT: - pathToProductFolderFromTagContent = "openai-assistant-product"; - break; - default: - break; + case NonStandardProductPackageConstants.PORTAL: + pathToProductFolderFromTagContent = "AxonIvyPortal/portal-product"; + break; + case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: + pathToProductFolderFromTagContent = "connectivity/connectivity-demos-product"; + break; + case NonStandardProductPackageConstants.ERROR_HANDLING: + pathToProductFolderFromTagContent = "error-handling/error-handling-demos-product"; + break; + case NonStandardProductPackageConstants.WORKFLOW_DEMO: + pathToProductFolderFromTagContent = "workflow/workflow-demos-product"; + break; + case NonStandardProductPackageConstants.MICROSOFT_365: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-connector"; + break; + case NonStandardProductPackageConstants.MICROSOFT_CALENDAR: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-calendar"; + break; + case NonStandardProductPackageConstants.MICROSOFT_TEAMS: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-chat"; + break; + case NonStandardProductPackageConstants.MICROSOFT_MAIL: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-mail"; + break; + case NonStandardProductPackageConstants.MICROSOFT_TODO: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-todo"; + break; + case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: + pathToProductFolderFromTagContent = "html-dialog/html-dialog-demos-product"; + break; + case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: + pathToProductFolderFromTagContent = "rule-engine/rule-engine-demos-product"; + break; + case NonStandardProductPackageConstants.OPENAI_CONNECTOR: + pathToProductFolderFromTagContent = "openai-connector-product"; + break; + case NonStandardProductPackageConstants.OPENAI_ASSISTANT: + pathToProductFolderFromTagContent = "openai-assistant-product"; + break; + default: + break; } return pathToProductFolderFromTagContent; } public static String getNonStandardImageFolder(String productId) { switch (productId) { - case NonStandardProductPackageConstants.EXCEL_IMPORTER: - pathToImageFolder = "doc"; - break; - case NonStandardProductPackageConstants.EXPRESS_IMPORTER, NonStandardProductPackageConstants.DEEPL_CONNECTOR: - pathToImageFolder = "img"; - break; - case NonStandardProductPackageConstants.GRAPHQL_DEMO: - pathToImageFolder = "assets"; - break; - case NonStandardProductPackageConstants.OPENAI_ASSISTANT: - pathToImageFolder = "docs"; - break; - default: - pathToImageFolder = "images"; - break; + case NonStandardProductPackageConstants.EXCEL_IMPORTER: + pathToImageFolder = "doc"; + break; + case NonStandardProductPackageConstants.EXPRESS_IMPORTER, NonStandardProductPackageConstants.DEEPL_CONNECTOR: + pathToImageFolder = "img"; + break; + case NonStandardProductPackageConstants.GRAPHQL_DEMO: + pathToImageFolder = "assets"; + break; + case NonStandardProductPackageConstants.OPENAI_ASSISTANT: + pathToImageFolder = "docs"; + break; + default: + pathToImageFolder = "images"; + break; } return pathToImageFolder; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java index 70d54b588..96236820f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java @@ -17,26 +17,25 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class DisplayValue { - private String locale; - private String value; + private String locale; + private String value; - @Override - public boolean equals(Object obj) { - if (!(obj instanceof DisplayValue)) { - return false; - } - DisplayValue other = (DisplayValue) obj; - EqualsBuilder builder = new EqualsBuilder(); - builder.append(value, other.getValue()); - builder.append(locale, other.locale); - return builder.isEquals(); - } + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DisplayValue other)) { + return false; + } + EqualsBuilder builder = new EqualsBuilder(); + builder.append(value, other.getValue()); + builder.append(locale, other.locale); + return builder.isEquals(); + } - @Override - public int hashCode() { - HashCodeBuilder builder = new HashCodeBuilder(); - builder.append(getValue()); - builder.append(getLocale()); - return builder.hashCode(); - } + @Override + public int hashCode() { + HashCodeBuilder builder = new HashCodeBuilder(); + builder.append(getValue()); + builder.append(getLocale()); + return builder.hashCode(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java index 5eb1769ce..663fab1dc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java @@ -1,6 +1,10 @@ package com.axonivy.market.model; import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -17,26 +21,35 @@ @Relation(collectionRelation = "feedbacks", itemRelation = "feedback") @JsonInclude(JsonInclude.Include.NON_NULL) public class FeedbackModel extends RepresentationModel { - private String id; - private String username; - private String userAvatarUrl; - private String userProvider; - private String productId; - private String content; - private Integer rating; - private Date createdAt; - private Date updatedAt; - - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + private String id; + private String userId; + private String username; + private String userAvatarUrl; + private String userProvider; + + @NotBlank(message = "Product id cannot be blank") + private String productId; + + @NotBlank(message = "Content cannot be blank") + @Size(max = 5, message = "Content length must be up to 250 characters") + private String content; + + @Min(value = 1, message = "Rating should not be less than 1") + @Max(value = 5, message = "Rating should not be greater than 5") + private Integer rating; + private Date createdAt; + private Date updatedAt; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((FeedbackModel) obj).getId()).isEquals(); + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; } + return new EqualsBuilder().append(id, ((FeedbackModel) obj).getId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/GitHubAccessTokenResponse.java b/marketplace-service/src/main/java/com/axonivy/market/model/GitHubAccessTokenResponse.java new file mode 100644 index 000000000..be3e89bd7 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/GitHubAccessTokenResponse.java @@ -0,0 +1,22 @@ +package com.axonivy.market.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class GitHubAccessTokenResponse { + @JsonProperty("error") + private String error; + + @JsonProperty("error_description") + private String errorDescription; + + @JsonProperty("access_token") + private String accessToken; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java index 3cb4ee1d7..4acdc23ad 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java @@ -13,6 +13,6 @@ @AllArgsConstructor @NoArgsConstructor public class MavenArtifactVersionModel { - private String version; - private List artifactsByVersion; + private String version; + private List artifactsByVersion; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java index 56706c4c1..b73f8dc66 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java @@ -8,5 +8,5 @@ @Setter @NoArgsConstructor public class Oauth2AuthorizationCode { - public String code; + private String code; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java index 0984f8765..79ea5b5bf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java @@ -18,23 +18,23 @@ @Relation(collectionRelation = "products", itemRelation = "product") @JsonInclude(Include.NON_NULL) public class ProductModel extends RepresentationModel { - private String id; - private MultilingualismValue names; - private MultilingualismValue shortDescriptions; - private String logoUrl; - private String type; - private List tags; + private String id; + private MultilingualismValue names; + private MultilingualismValue shortDescriptions; + private String logoUrl; + private String type; + private List tags; - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((ProductModel) obj).getId()).isEquals(); - } + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((ProductModel) obj).getId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java index 30969ab97..faf1fc553 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java @@ -6,5 +6,5 @@ @Repository public interface UserRepository extends MongoRepository { - User searchByGitHubId(String gitHubId); + User searchByGitHubId(String gitHubId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java index 5621c2d84..9e21e7453 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java +++ b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java @@ -11,7 +11,7 @@ public class ScheduledTasks { private static final String SCHEDULING_TASK_PRODUCTS_CRON = "0 0 0/1 ? * *"; - private ProductService productService; + private final ProductService productService; public ScheduledTasks(ProductService productService) { this.productService = productService; diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java index 1e8988f22..b7d1785b5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java @@ -2,6 +2,7 @@ import com.axonivy.market.entity.Feedback; import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.FeedbackModel; import com.axonivy.market.model.ProductRating; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -10,8 +11,12 @@ public interface FeedbackService { Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException; + Feedback findFeedback(String id) throws NotFoundException; + Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException; - Feedback upsertFeedback(Feedback feedback) throws NotFoundException; + + Feedback upsertFeedback(FeedbackModel feedback) throws NotFoundException; + List getProductRatingById(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java index 49f1c9d44..76f29ca17 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java @@ -5,6 +5,8 @@ public interface JwtService { String generateToken(User user); + boolean validateToken(String token); + Claims getClaimsFromToken(String token); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java index b6c064b4f..d47af915b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java @@ -7,6 +7,8 @@ public interface UserService { List getAllUsers(); + User createUser(User user); + User findUser(String id) throws NotFoundException; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java index 214e7b669..19755be9c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java @@ -6,12 +6,12 @@ public interface VersionService { - List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion); + List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion); - List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactId); + List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactId); - String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactId); + String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactId); - List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, - String designerVersion); + List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, + String designerVersion); } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java index 74ae9a998..4305fe0e8 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -3,18 +3,16 @@ import com.axonivy.market.entity.Feedback; import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.FeedbackModel; import com.axonivy.market.model.ProductRating; import com.axonivy.market.repository.FeedbackRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.repository.UserRepository; import com.axonivy.market.service.FeedbackService; -import com.axonivy.market.service.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -27,7 +25,8 @@ public class FeedbackServiceImpl implements FeedbackService { private final UserRepository userRepository; private final ProductRepository productRepository; - public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository userRepository, ProductRepository productRepository, UserService userService) { + public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository userRepository, + ProductRepository productRepository) { this.feedbackRepository = feedbackRepository; this.userRepository = userRepository; this.productRepository = productRepository; @@ -41,30 +40,36 @@ public Page findFeedbacks(String productId, Pageable pageable) throws @Override public Feedback findFeedback(String id) throws NotFoundException { - return feedbackRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, "Not found feedback with id: " + id)); + return feedbackRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, "Not found feedback with id: " + id)); } @Override public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException { - userRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + userId)); + validateUserExists(userId); validateProductExists(productId); Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, productId); if (existingUserFeedback == null) { - throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId)); + throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, + String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId)); } return existingUserFeedback; } @Override - public Feedback upsertFeedback(Feedback feedback) throws NotFoundException { - userRepository.findById(feedback.getUserId()) - .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND,"Not found user with id: " + feedback.getUserId())); + public Feedback upsertFeedback(FeedbackModel feedback) throws NotFoundException { + validateUserExists(feedback.getUserId()); - Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(feedback.getUserId(), feedback.getProductId()); + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(feedback.getUserId(), + feedback.getProductId()); if (existingUserFeedback == null) { - return feedbackRepository.save(feedback); + Feedback newFeedback = new Feedback(); + newFeedback.setUserId(feedback.getUserId()); + newFeedback.setProductId(feedback.getProductId()); + newFeedback.setRating(feedback.getRating()); + newFeedback.setContent(feedback.getContent()); + return feedbackRepository.save(newFeedback); } else { existingUserFeedback.setRating(feedback.getRating()); existingUserFeedback.setContent(feedback.getContent()); @@ -78,25 +83,28 @@ public List getProductRatingById(String productId) { int totalFeedbacks = feedbacks.size(); if (totalFeedbacks == 0) { - return IntStream.rangeClosed(1, 5) - .mapToObj(star -> new ProductRating(star, 0, 0)) - .collect(Collectors.toList()); + return IntStream.rangeClosed(1, 5).mapToObj(star -> new ProductRating(star, 0, 0)).toList(); } Map ratingCountMap = feedbacks.stream() .collect(Collectors.groupingBy(Feedback::getRating, Collectors.counting())); - return IntStream.rangeClosed(1, 5) - .mapToObj(star -> { - long count = ratingCountMap.getOrDefault(star, 0L); - int percent = (int) ((count * 100) / totalFeedbacks); - return new ProductRating(star, Math.toIntExact(count), percent); - }) - .collect(Collectors.toList()); + return IntStream.rangeClosed(1, 5).mapToObj(star -> { + long count = ratingCountMap.getOrDefault(star, 0L); + int percent = (int) ((count * 100) / totalFeedbacks); + return new ProductRating(star, Math.toIntExact(count), percent); + }).toList(); } private void validateProductExists(String productId) throws NotFoundException { - productRepository.findById(productId) - .orElseThrow(() -> new NotFoundException(ErrorCode.PRODUCT_NOT_FOUND, "Not found product with id: " + productId)); + if (productRepository.findById(productId).isEmpty()) { + throw new NotFoundException(ErrorCode.PRODUCT_NOT_FOUND, "Not found product with id: " + productId); + } + } + + private void validateUserExists(String userId) { + if (userRepository.findById(userId).isEmpty()) { + throw new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + userId); + } } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java index 979bec72c..45526c950 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java @@ -26,13 +26,9 @@ public String generateToken(User user) { Map claims = new HashMap<>(); claims.put("name", user.getName()); claims.put("username", user.getUsername()); - return Jwts.builder() - .setClaims(claims) - .setSubject(user.getId()) - .setIssuedAt(new Date()) + return Jwts.builder().setClaims(claims).setSubject(user.getId()).setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration * 86400000)) - .signWith(SignatureAlgorithm.HS512, secret) - .compact(); + .signWith(SignatureAlgorithm.HS512, secret).compact(); } public boolean validateToken(String token) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 04de791d5..4a8afd880 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -1,38 +1,5 @@ package com.axonivy.market.service.impl; -import static java.util.Optional.ofNullable; -import static org.apache.commons.lang3.StringUtils.EMPTY; - -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Random; - -import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHTag; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.GitHubRepoMeta; @@ -50,9 +17,40 @@ import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ProductService; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; - import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.EMPTY; @Log4j2 @Service @@ -89,22 +87,22 @@ public Page findProducts(String type, String keyword, String language, final var searchPageable = refinePagination(language, pageable); Page result = Page.empty(); switch (typeOption) { - case ALL: - if (StringUtils.isBlank(keyword)) { - result = productRepository.findAll(searchPageable); - } else { - result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); - } - break; - case CONNECTORS, UTILITIES, SOLUTIONS: - if (StringUtils.isBlank(keyword)) { - result = productRepository.findByType(typeOption.getCode(), searchPageable); - } else { - result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); - } - break; - default: - break; + case ALL: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findAll(searchPageable); + } else { + result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); + } + break; + case CONNECTORS, UTILITIES, SOLUTIONS: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findByType(typeOption.getCode(), searchPageable); + } else { + result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); + } + break; + default: + break; } return result; } @@ -159,8 +157,8 @@ private void syncRepoMetaDataStatus() { if (lastGHCommit == null) { return; } - String repoURL = - Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath).orElse(EMPTY); + String repoURL = Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath) + .orElse(EMPTY); marketRepoMeta.setRepoURL(repoURL); marketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); marketRepoMeta.setLastSHA1(lastGHCommit.getSHA1()); @@ -209,34 +207,34 @@ private void updateLatestChangeToProductsFromGithubRepo() { private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { Product result = null; switch (file.getStatus()) { - case MODIFIED, ADDED: - result = productRepository.findByMarketDirectoryRegex(parentPath); - if (result != null) { - result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); - productRepository.save(result); - } - break; - case REMOVED: - result = productRepository.findByLogoUrl(product.getLogoUrl()); - if (result != null) { - productRepository.deleteById(result.getId()); - } - break; - default: - break; + case MODIFIED, ADDED: + result = productRepository.findByMarketDirectoryRegex(parentPath); + if (result != null) { + result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); + productRepository.save(result); + } + break; + case REMOVED: + result = productRepository.findByLogoUrl(product.getLogoUrl()); + if (result != null) { + productRepository.deleteById(result.getId()); + } + break; + default: + break; } } private void modifyProductByMetaContent(GitHubFile file, Product product) { switch (file.getStatus()) { - case MODIFIED, ADDED: - productRepository.save(product); - break; - case REMOVED: - productRepository.deleteById(product.getId()); - break; - default: - break; + case MODIFIED, ADDED: + productRepository.save(product); + break; + case REMOVED: + productRepository.deleteById(product.getId()); + break; + default: + break; } } @@ -256,14 +254,14 @@ private Pageable refinePagination(String language, Pageable pageable) { private boolean isLastGithubCommitCovered() { boolean isLastCommitCovered = false; - long lastCommitTime = 0l; + long lastCommitTime = 0L; marketRepoMeta = gitHubRepoMetaRepository.findByRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); if (marketRepoMeta != null) { lastCommitTime = marketRepoMeta.getLastChange(); } lastGHCommit = axonIvyMarketRepoService.getLastCommit(lastCommitTime); - if (lastGHCommit != null && marketRepoMeta != null - && StringUtils.equals(lastGHCommit.getSHA1(), marketRepoMeta.getLastSHA1())) { + if (lastGHCommit != null && marketRepoMeta != null && StringUtils.equals(lastGHCommit.getSHA1(), + marketRepoMeta.getLastSHA1())) { isLastCommitCovered = true; } return isLastCommitCovered; @@ -309,8 +307,8 @@ private void updateProductFromReleaseTags(Product product) { List productModuleContents = new ArrayList<>(); for (GHTag ghtag : tags) { - ProductModuleContent productModuleContent = - axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()); + ProductModuleContent productModuleContent = axonIvyProductRepoService.getReadmeAndProductContentsFromTag( + product, productRepo, ghtag.getName()); productModuleContents.add(productModuleContent); } product.setProductModuleContents(productModuleContents); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java index 750bf3995..0d6c534bb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java @@ -25,7 +25,8 @@ public List getAllUsers() { @Override public User findUser(String id) throws NotFoundException { - return userRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + id)); + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + id)); } @Override diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index 06d07123d..80f87948d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -1,22 +1,22 @@ package com.axonivy.market.service.impl; +import com.axonivy.market.comparator.ArchivedArtifactsComparator; +import com.axonivy.market.comparator.LatestVersionComparator; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.entity.MavenArtifactVersion; import com.axonivy.market.entity.Product; import com.axonivy.market.github.model.ArchivedArtifact; import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.VersionService; -import com.axonivy.market.comparator.ArchivedArtifactsComparator; -import com.axonivy.market.comparator.LatestVersionComparator; import com.axonivy.market.util.XmlReaderUtils; import lombok.Getter; import lombok.extern.log4j.Log4j2; @@ -27,7 +27,15 @@ import org.springframework.util.CollectionUtils; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; @Log4j2 @@ -94,8 +102,8 @@ public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, boolean isNewVersionDetected = false; for (String version : versionsToDisplay) { List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); - List productArtifactModels = - proceedDataCache.getProductArtifactWithVersionReleased().get(version); + List productArtifactModels = proceedDataCache.getProductArtifactWithVersionReleased() + .get(version); if (productArtifactModels == null) { isNewVersionDetected = true; productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); @@ -107,8 +115,8 @@ public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, } public List updateArtifactsInVersionWithProductArtifact(String version) { - List productArtifactModels = - convertMavenArtifactsToModels(getProductJsonByVersion(version), version); + List productArtifactModels = convertMavenArtifactsToModels(getProductJsonByVersion(version), + version); proceedDataCache.getVersions().add(version); proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); return productArtifactModels; @@ -126,8 +134,9 @@ public List getProductMetaArtifacts(String productId) { public void sanitizeMetaArtifactBeforeHandle() { artifactsFromMeta.remove(metaProductArtifact); artifactsFromMeta.forEach(artifact -> { - List archivedArtifacts = new ArrayList<>(Optional.ofNullable(artifact.getArchivedArtifacts()) - .orElse(Collections.emptyList()).stream().sorted(new ArchivedArtifactsComparator()).toList()); + List archivedArtifacts = new ArrayList<>( + Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).stream() + .sorted(new ArchivedArtifactsComparator()).toList()); Collections.reverse(archivedArtifacts); archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); }); @@ -152,9 +161,10 @@ public List getVersionsFromMavenArtifacts() { for (MavenArtifact artifact : artifactsFromMeta) { versions.addAll( getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), artifact.getArtifactId())); - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()) - .forEach(archivedArtifact -> versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), - archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()))); + Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).forEach( + archivedArtifact -> versions.addAll( + getVersionsFromArtifactDetails(artifact.getRepoUrl(), archivedArtifact.getGroupId(), + archivedArtifact.getArtifactId()))); } List versionList = new ArrayList<>(versions); versionList.sort(new LatestVersionComparator()); @@ -206,8 +216,9 @@ public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, St } else { bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); } - return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) - && isReleasedVersion(currentVersion) && getBugfixVersion(currentVersion).equals(bugfixVersion)); + return versions.stream().noneMatch( + currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) && getBugfixVersion( + currentVersion).equals(bugfixVersion)); } public boolean isSnapshotVersion(String version) { @@ -231,8 +242,8 @@ public List getProductJsonByVersion(String version) { String versionTag = getVersionTag(version); productJsonFilePath = buildProductJsonFilePath(); try { - GHContent productJsonContent = - gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, versionTag); + GHContent productJsonContent = gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, + versionTag); if (Objects.isNull(productJsonContent)) { return result; } @@ -255,8 +266,8 @@ public String getVersionTag(String version) { public String buildProductJsonFilePath() { String pathToProductFolderFromTagContent = metaProductArtifact.getArtifactId(); GitHubUtils.getNonStandardProductFilePath(productId); - productJsonFilePath = - String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, pathToProductFolderFromTagContent); + productJsonFilePath = String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, + pathToProductFolderFromTagContent); return productJsonFilePath; } @@ -286,8 +297,8 @@ public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, Str String groupIdByVersion = artifact.getGroupId(); String artifactIdByVersion = artifact.getArtifactId(); String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - ArchivedArtifact archivedArtifactBestMatchVersion = - findArchivedArtifactInfoBestMatchWithVersion(artifact.getArtifactId(), version); + ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion( + artifact.getArtifactId(), version); if (Objects.nonNull(archivedArtifactBestMatchVersion)) { groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java index d49802145..33fe8e20e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java @@ -24,7 +24,8 @@ public class XmlReaderUtils { private static final RestTemplate restTemplate = new RestTemplate(); - private XmlReaderUtils() {} + private XmlReaderUtils() { + } public static List readXMLFromUrl(String url) { List versions = new ArrayList<>(); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java index 55817625b..ea02160f9 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java @@ -3,6 +3,7 @@ import com.axonivy.market.assembler.FeedbackModelAssembler; import com.axonivy.market.entity.Feedback; import com.axonivy.market.entity.User; +import com.axonivy.market.model.FeedbackModel; import com.axonivy.market.service.FeedbackService; import com.axonivy.market.service.JwtService; import com.axonivy.market.service.UserService; @@ -120,6 +121,7 @@ void testFindFeedbackByUserIdAndProductId() { @Test void testCreateFeedback() { + FeedbackModel mockFeedbackModel = createFeedbackModelMock(); Feedback mockFeedback = createFeedbackMock(); Claims mockClaims = createMockClaims(); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -128,7 +130,7 @@ void testCreateFeedback() { when(jwtService.getClaimsFromToken(TOKEN_SAMPLE)).thenReturn(mockClaims); when(service.upsertFeedback(any())).thenReturn(mockFeedback); - var result = feedbackController.createFeedback(mockFeedback, "Bearer " + TOKEN_SAMPLE); + var result = feedbackController.createFeedback(mockFeedbackModel, "Bearer " + TOKEN_SAMPLE); assertEquals(HttpStatus.CREATED, result.getStatusCode()); assertTrue(result.getHeaders().getLocation().toString().contains(mockFeedback.getId())); } @@ -143,6 +145,16 @@ private Feedback createFeedbackMock() { return mockFeedback; } + private FeedbackModel createFeedbackModelMock() { + FeedbackModel mockFeedback = new FeedbackModel(); + mockFeedback.setId(FEEDBACK_ID_SAMPLE); + mockFeedback.setUserId(USER_ID_SAMPLE); + mockFeedback.setProductId(PRODUCT_ID_SAMPLE); + mockFeedback.setContent("Great product!"); + mockFeedback.setRating(5); + return mockFeedback; + } + private User createUserMock() { User mockUser = new User(); mockUser.setId(USER_ID_SAMPLE); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java index eff891be0..efcc39756 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -2,6 +2,7 @@ import com.axonivy.market.entity.User; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.GitHubAccessTokenResponse; import com.axonivy.market.model.Oauth2AuthorizationCode; import com.axonivy.market.service.JwtService; import org.junit.jupiter.api.BeforeEach; @@ -44,7 +45,7 @@ void testGitHubLogin() { User user = createUserMock(); String jwtToken = "sampleJwtToken"; - when(gitHubService.getAccessToken(any(), any(), any())).thenReturn(Map.of("access_token", accessToken)); + when(gitHubService.getAccessToken(any(), any(), any())).thenReturn(createGitHubAccessTokenResponseMock()); when(gitHubService.getAndUpdateUser(accessToken)).thenReturn(user); when(jwtService.generateToken(user)).thenReturn(jwtToken); @@ -63,4 +64,10 @@ private User createUserMock() { user.setProvider("github"); return user; } + + private GitHubAccessTokenResponse createGitHubAccessTokenResponseMock() { + GitHubAccessTokenResponse gitHubAccessTokenResponse = new GitHubAccessTokenResponse(); + gitHubAccessTokenResponse.setAccessToken("sampleAccessToken"); + return gitHubAccessTokenResponse; + } } \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 0d984281e..079526960 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -6,7 +6,6 @@ import com.axonivy.market.enums.SortOption; import com.axonivy.market.enums.TypeOption; import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.model.ProductRating; import com.axonivy.market.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -113,12 +112,4 @@ private Product createProductMock() { mockProduct.setTags(List.of("AI")); return mockProduct; } - - private ProductRating createProductRatingMock() { - ProductRating productRatingMock = new ProductRating(); - productRatingMock.setStarRating(1); - productRatingMock.setPercent(10); - productRatingMock.setCommentNumber(5); - return productRatingMock; - } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 0ac125d07..3551a82cf 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -1,13 +1,12 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.Objects; - +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.MultilingualismValue; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,13 +17,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import com.axonivy.market.assembler.ProductDetailModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.service.ProductService; -import com.axonivy.market.service.VersionService; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { @@ -48,8 +47,8 @@ class ProductDetailsControllerTest { void testProductDetails() { Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); Mockito.when(detailModelAssembler.toModel(mockProduct(), null)).thenReturn(createProductMockWithDetails()); - ResponseEntity mockExpectedResult = - new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); + ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), + HttpStatus.OK); ResponseEntity result = productDetailsController.findProductDetails(DOCKER_CONNECTOR_ID); @@ -64,11 +63,11 @@ void testProductDetails() { void testProductDetailsWithVersion() { Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG)).thenReturn(createProductMockWithDetails()); - ResponseEntity mockExpectedResult = - new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); + ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), + HttpStatus.OK); - ResponseEntity result = - productDetailsController.findProductDetailsByVersion(DOCKER_CONNECTOR_ID, TAG); + ResponseEntity result = productDetailsController.findProductDetailsByVersion( + DOCKER_CONNECTOR_ID, TAG); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(result, mockExpectedResult); @@ -80,10 +79,10 @@ void testProductDetailsWithVersion() { void testFindProductVersionsById() { List models = List.of(new MavenArtifactVersionModel()); Mockito.when( - versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())) + versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())) .thenReturn(models); - ResponseEntity> result = - productDetailsController.findProductVersionsById("protal", true, "10.0.1"); + ResponseEntity> result = productDetailsController.findProductVersionsById("protal", + true, "10.0.1"); Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()); Assertions.assertEquals(1, Objects.requireNonNull(result.getBody()).size()); Assertions.assertEquals(models, result.getBody()); diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index dd770b752..2446c18c1 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -1,15 +1,7 @@ package com.axonivy.market.factory; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; - +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.entity.Product; import com.axonivy.market.github.model.Meta; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -17,13 +9,19 @@ import org.kohsuke.github.GHContent; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.entity.Product; +import java.io.IOException; +import java.io.InputStream; + +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductFactoryTest { - private static final String DUMMY_LOGO_URL = - "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; + private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; @Test void testMappingByGHContent() throws IOException { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java index 51a510357..5237742c3 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java @@ -3,6 +3,7 @@ import com.axonivy.market.entity.Feedback; import com.axonivy.market.entity.User; import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.FeedbackModel; import com.axonivy.market.model.ProductRating; import com.axonivy.market.repository.FeedbackRepository; import com.axonivy.market.repository.ProductRepository; @@ -20,9 +21,13 @@ import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class FeedbackServiceImplTest { @@ -50,7 +55,9 @@ void testFindFeedbacks_ProductNotFound() { when(productRepository.findById(productId)).thenReturn(Optional.empty()); - assertThrows(NotFoundException.class, () -> feedbackService.findFeedbacks(productId, Pageable.unpaged())); + Pageable unpaged = Pageable.unpaged(); + + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbacks(productId, unpaged)); verify(productRepository, times(1)).findById(productId); verify(feedbackRepository, never()).searchByProductId(any(), any()); @@ -99,7 +106,8 @@ void testFindFeedbackByUserIdAndProductId_UserNotFound() { when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); // Test and verify exception - assertThrows(NotFoundException.class, () -> feedbackService.findFeedbackByUserIdAndProductId(nonExistingUserId, productId)); + assertThrows(NotFoundException.class, + () -> feedbackService.findFeedbackByUserIdAndProductId(nonExistingUserId, productId)); // Verify interactions verify(userRepository, times(1)).findById(nonExistingUserId); @@ -109,42 +117,54 @@ void testFindFeedbackByUserIdAndProductId_UserNotFound() { @Test void testUpsertFeedback_NewFeedback() throws NotFoundException { // Mock data + FeedbackModel newFeedbackModel = new FeedbackModel(); + newFeedbackModel.setUserId("user123"); + newFeedbackModel.setProductId("product123"); + newFeedbackModel.setContent("Great product!"); + newFeedbackModel.setRating(5); + Feedback newFeedback = new Feedback(); - newFeedback.setUserId("user123"); - newFeedback.setProductId("product123"); - newFeedback.setContent("Great product!"); - newFeedback.setRating(5); + newFeedback.setUserId(newFeedbackModel.getUserId()); + newFeedback.setProductId(newFeedbackModel.getProductId()); + newFeedback.setContent(newFeedbackModel.getContent()); + newFeedback.setRating(newFeedbackModel.getRating()); User u = new User(); - u.setId(newFeedback.getUserId()); - when(userRepository.findById(newFeedback.getUserId())).thenReturn(Optional.of(u)); - when(feedbackRepository.findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId())).thenReturn(null); - when(feedbackRepository.save(newFeedback)).thenReturn(newFeedback); + u.setId(newFeedbackModel.getUserId()); + when(userRepository.findById(newFeedbackModel.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(newFeedbackModel.getUserId(), + newFeedbackModel.getProductId())).thenReturn(null); + when(feedbackRepository.save(any(Feedback.class))).thenReturn(newFeedback); // Test method - Feedback result = feedbackService.upsertFeedback(newFeedback); + Feedback result = feedbackService.upsertFeedback(newFeedbackModel); // Verify assertEquals(newFeedback, result); - verify(userRepository, times(1)).findById(newFeedback.getUserId()); - verify(feedbackRepository, times(1)).findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId()); - verify(feedbackRepository, times(1)).save(newFeedback); } @Test void testUpsertFeedback_UpdateFeedback() throws NotFoundException { // Mock data + FeedbackModel existingFeedbackModel = new FeedbackModel(); + existingFeedbackModel.setId("existingFeedback123"); + existingFeedbackModel.setUserId("user123"); + existingFeedbackModel.setProductId("product123"); + existingFeedbackModel.setContent("Good product! Very well!"); + existingFeedbackModel.setRating(5); + Feedback existingFeedback = new Feedback(); existingFeedback.setId("existingFeedback123"); existingFeedback.setUserId("user123"); existingFeedback.setProductId("product123"); - existingFeedback.setContent("Good product!"); - existingFeedback.setRating(4); + existingFeedback.setContent("Bad product!"); + existingFeedback.setRating(1); User u = new User(); u.setId(existingFeedback.getUserId()); when(userRepository.findById(existingFeedback.getUserId())).thenReturn(Optional.of(u)); - when(feedbackRepository.findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId())).thenReturn(existingFeedback); + when(feedbackRepository.findByUserIdAndProductId(existingFeedback.getUserId(), + existingFeedback.getProductId())).thenReturn(existingFeedback); when(feedbackRepository.save(existingFeedback)).thenReturn(existingFeedback); // Test method @@ -152,17 +172,18 @@ void testUpsertFeedback_UpdateFeedback() throws NotFoundException { updatedFeedback.setId(existingFeedback.getId()); updatedFeedback.setUserId(existingFeedback.getUserId()); updatedFeedback.setProductId(existingFeedback.getProductId()); - updatedFeedback.setContent("Excellent product!"); + updatedFeedback.setContent("Good product! Very well!"); updatedFeedback.setRating(5); - Feedback result = feedbackService.upsertFeedback(updatedFeedback); + Feedback result = feedbackService.upsertFeedback(existingFeedbackModel); // Verify assertEquals(updatedFeedback.getId(), result.getId()); assertEquals(updatedFeedback.getContent(), result.getContent()); assertEquals(updatedFeedback.getRating(), result.getRating()); verify(userRepository, times(1)).findById(existingFeedback.getUserId()); - verify(feedbackRepository, times(1)).findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(existingFeedback.getUserId(), + existingFeedback.getProductId()); verify(feedbackRepository, times(1)).save(existingFeedback); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java index 0e1f06f4b..5eda06723 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -6,8 +6,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHCommit.File; -import org.kohsuke.github.*; +import org.kohsuke.github.GHCompare; import org.kohsuke.github.GHCompare.Commit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.PagedIterable; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java index fa0104d4a..1c7bdf164 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java @@ -11,7 +11,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.*; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.kohsuke.github.PagedIterable; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -26,9 +30,14 @@ import java.util.Iterator; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GHAxonIvyProductRepoServiceImplTest { @@ -104,8 +113,8 @@ void testExtractMavenArtifactFromJsonNode() { createListNodeForDataNoteByName(nodeName); MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, - isDependency); + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl) + .createArtifactFromJsonNode(childNode, null, isDependency); axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); @@ -116,8 +125,8 @@ void testExtractMavenArtifactFromJsonNode() { nodeName = ProductJsonConstants.PROJECTS; createListNodeForDataNoteByName(nodeName); - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, - isDependency); + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl) + .createArtifactFromJsonNode(childNode, null, isDependency); axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); @@ -190,8 +199,7 @@ void testGetOrganization() throws IOException { @Test void testGetReadmeAndProductContentsFromTag() throws IOException { - String readmeContentWithImage = - "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + String readmeContentWithImage = "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; GHContent mockContent = createMockProductFolderWithProductJson(); @@ -213,8 +221,7 @@ void testGetReadmeAndProductContentsFromTag() throws IOException { @Test void testGetReadmeAndProductContentFromTag_ImageFromFolder() throws IOException { - String readmeContentWithImageFolder = - "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (./images/image.png)"; + String readmeContentWithImageFolder = "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (./images/image.png)"; GHContent mockImageFile = mock(GHContent.class); when(mockImageFile.getName()).thenReturn(ReadmeConstants.IMAGES, IMAGE_NAME); @@ -324,9 +331,10 @@ private static InputStream getMockInputStream() { } private static InputStream getMockInputStreamWithOutProjectAndDependency() { - String jsonContent = "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" - + " \"repositories\": [\n" + " {\n" + " \"url\": \"http://example.com/repo\"\n" - + " }\n" + " ]\n" + " }\n" + " }\n" + " ]\n" + "}"; + String jsonContent = + "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" + " \"repositories\": [\n" + + " {\n" + " \"url\": \"http://example.com/repo\"\n" + " }\n" + " ]\n" + + " }\n" + " }\n" + " ]\n" + "}"; return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); } @@ -357,10 +365,10 @@ private GHContent createMockProductFolderWithProductJson() throws IOException { GHContent mockContent2 = createMockProductJson(); - when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)) - .thenReturn(List.of(mockContent, mockContent2)); - when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)) - .thenReturn(List.of(mockContent, mockContent2)); + when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)).thenReturn( + List.of(mockContent, mockContent2)); + when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)).thenReturn( + List.of(mockContent, mockContent2)); return mockContent; } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index bbd8416fa..c4e0c07ea 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -19,7 +19,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GitHubServiceImplTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java index 4eb8ffcec..d5c2b7457 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java @@ -11,84 +11,87 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(MockitoExtension.class) class JwtServiceImplTest { - private static final String SECRET = "mySecret"; - private static final long EXPIRATION = 7L; // 7 days - - @InjectMocks - private JwtServiceImpl jwtService; - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(jwtService, "secret", SECRET); - ReflectionTestUtils.setField(jwtService, "expiration", EXPIRATION); - } - - @Test - void testGenerateToken() { - User user = new User(); - user.setId("123"); - user.setName("John Doe"); - user.setUsername("johndoe"); - - String token = jwtService.generateToken(user); - - assertNotNull(token); - assertFalse(token.isEmpty()); - - Claims claims = jwtService.getClaimsFromToken(token); - assertEquals("123", claims.getSubject()); - assertEquals("John Doe", claims.get("name")); - assertEquals("johndoe", claims.get("username")); - } - - @Test - void testValidateToken() { - User user = new User(); - user.setId("123"); - user.setName("John Doe"); - user.setUsername("johndoe"); - - String validToken = jwtService.generateToken(user); - assertTrue(jwtService.validateToken(validToken)); - - String invalidToken = "invalid.token.here"; - assertFalse(jwtService.validateToken(invalidToken)); - } - - @Test - void testGetClaimsFromToken() { - User user = new User(); - user.setId("123"); - user.setName("John Doe"); - user.setUsername("johndoe"); - - String token = jwtService.generateToken(user); - - Claims claims = jwtService.getClaimsFromToken(token); - assertNotNull(claims); - assertEquals("123", claims.getSubject()); - assertEquals("John Doe", claims.get("name")); - assertEquals("johndoe", claims.get("username")); - } - - @Test - void testGetClaimsJws() { - User user = new User(); - user.setId("123"); - user.setName("John Doe"); - user.setUsername("johndoe"); - - String token = jwtService.generateToken(user); - - Jws claimsJws = jwtService.getClaimsJws(token); - assertNotNull(claimsJws); - assertNotNull(claimsJws.getBody()); - assertEquals("123", claimsJws.getBody().getSubject()); - } + private static final String SECRET = "mySecret"; + private static final long EXPIRATION = 7L; // 7 days + + @InjectMocks + private JwtServiceImpl jwtService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(jwtService, "secret", SECRET); + ReflectionTestUtils.setField(jwtService, "expiration", EXPIRATION); + } + + @Test + void testGenerateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + assertNotNull(token); + assertFalse(token.isEmpty()); + + Claims claims = jwtService.getClaimsFromToken(token); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testValidateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String validToken = jwtService.generateToken(user); + assertTrue(jwtService.validateToken(validToken)); + + String invalidToken = "invalid.token.here"; + assertFalse(jwtService.validateToken(invalidToken)); + } + + @Test + void testGetClaimsFromToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Claims claims = jwtService.getClaimsFromToken(token); + assertNotNull(claims); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testGetClaimsJws() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Jws claimsJws = jwtService.getClaimsJws(token); + assertNotNull(claimsJws); + assertNotNull(claimsJws.getBody()); + assertEquals("123", claimsJws.getBody().getSubject()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 841cfc7f5..aa02a2c9f 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -1,33 +1,21 @@ package com.axonivy.market.service; -import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.FileStatus; +import com.axonivy.market.enums.FileType; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.github.model.GitHubFile; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.MultilingualismValue; +import com.axonivy.market.repository.GitHubRepoMetaRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.impl.ProductServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -50,22 +38,33 @@ import org.springframework.data.domain.Sort; import org.springframework.test.util.ReflectionTestUtils; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.GitHubRepoMeta; -import com.axonivy.market.entity.Product; -import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.enums.FileStatus; -import com.axonivy.market.enums.FileType; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.github.model.GitHubFile; -import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.repository.GitHubRepoMetaRepository; -import com.axonivy.market.repository.ProductRepository; -import com.axonivy.market.service.impl.ProductServiceImpl; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductServiceImplTest { @@ -73,8 +72,8 @@ class ProductServiceImplTest { private static final String SAMPLE_PRODUCT_ID = "amazon-comprehend"; private static final String SAMPLE_PRODUCT_NAME = "Amazon Comprehend"; private static final long LAST_CHANGE_TIME = 1718096290000l; - private static final Pageable PAGEABLE = - PageRequest.of(0, 20, Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); + private static final Pageable PAGEABLE = PageRequest.of(0, 20, + Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; public static final String RELEASE_TAG = "v10.0.2"; private String keyword; @@ -250,9 +249,10 @@ void testFindAllProductsWithKeyword() throws IOException { verify(productRepository).findAll(any(Pageable.class)); // Test has keyword - when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(), any(Pageable.class))) - .thenReturn(new PageImpl<>(mockResultReturn.stream() - .filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME)).collect(Collectors.toList()))); + when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(), any(Pageable.class))).thenReturn( + new PageImpl<>( + mockResultReturn.stream().filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME)) + .collect(Collectors.toList()))); // Executes result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); verify(productRepository).findAll(any(Pageable.class)); @@ -260,14 +260,10 @@ void testFindAllProductsWithKeyword() throws IOException { assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); // Test has keyword and type is connector - when( - productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))) - .thenReturn( - new PageImpl<>( - mockResultReturn.stream() - .filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) - && product.getType().equals(TypeOption.CONNECTORS.getCode())) - .collect(Collectors.toList()))); + when(productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))).thenReturn(new PageImpl<>( + mockResultReturn.stream().filter( + product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) && product.getType() + .equals(TypeOption.CONNECTORS.getCode())).collect(Collectors.toList()))); // Executes result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); assertTrue(result.hasContent()); @@ -279,8 +275,8 @@ void testSyncProductsFirstTime() throws IOException { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); - when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())) - .thenReturn(mockReadmeProductContent()); + when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())).thenReturn( + mockReadmeProductContent()); when(gitHubService.getRepository(any())).thenReturn(ghRepository); PagedIterable pagedIterable = mock(PagedIterable.class); when(ghRepository.listTags()).thenReturn(pagedIterable); @@ -330,8 +326,8 @@ void testSearchProducts() { String type = TypeOption.ALL.getOption(); keyword = "on"; langague = "en"; - when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)) - .thenReturn(mockResultReturn); + when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)).thenReturn( + mockResultReturn); var result = productService.findProducts(type, keyword, langague, simplePageable); assertEquals(result, mockResultReturn); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index 70e4d0771..bdc720dfc 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -10,7 +10,7 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; -@SpringBootTest(properties = { "marketPlace-installation-url=D:/marketplace-installation.json" }) +@SpringBootTest(properties = { "MARKETPLACE_INSTALLATION_URL=D:/marketplace-installation.json" }) class SchedulingTasksTest { @SpyBean diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java index ce3ef83ee..60ffb9248 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java @@ -19,16 +19,24 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; -import org.mockito.*; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class VersionServiceImplTest { @@ -81,13 +89,13 @@ void testGetArtifactsAndVersionToDisplay() { String targetVersion = "10.0.10"; setUpArtifactFromMeta(); when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); - when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())) - .thenReturn(List.of(targetVersion)); + when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())).thenReturn( + List.of(targetVersion)); when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); ArrayList artifactsInVersion = new ArrayList<>(); artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(artifactsInVersion); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())).thenReturn( + artifactsInVersion); Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); MavenArtifactVersion proceededData = new MavenArtifactVersion(); @@ -110,8 +118,8 @@ void testHandleArtifactForVersionToDisplay() { result = new ArrayList<>(); ArrayList artifactsInVersion = new ArrayList<>(); artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(artifactsInVersion); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())).thenReturn( + artifactsInVersion); Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); Assertions.assertEquals(1, result.size()); Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); @@ -141,8 +149,8 @@ void testUpdateArtifactsInVersionWithProductArtifact() { MavenArtifactModel artifactModel = new MavenArtifactModel(); List mockMavenArtifactModels = List.of(artifactModel); when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(mockMavenArtifactModels); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())).thenReturn( + mockMavenArtifactModels); Assertions.assertEquals(mockMavenArtifactModels, versionService.updateArtifactsInVersionWithProductArtifact(version)); Assertions.assertEquals(1, proceedDataCache.getVersions().size()); @@ -208,8 +216,8 @@ void getVersionsFromMavenArtifacts() { versionFromArchivedArtifact.add("10.0.2"); versionFromArchivedArtifact.add("10.0.1"); artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)) - .thenReturn(versionFromArchivedArtifact); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)).thenReturn( + versionFromArchivedArtifact); versionFromArtifact.addAll(versionFromArchivedArtifact); Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); } @@ -230,10 +238,10 @@ void testGetVersionsFromArtifactDetails() { try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), + versionFromArtifact); } - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), - versionFromArtifact); } @Test @@ -241,8 +249,7 @@ void testBuildMavenMetadataUrlFromArtifact() { String repoUrl = "https://maven.axonivy.com"; String groupId = "com.axonivy.connector.adobe.acrobat.sign"; String artifactId = "adobe-acrobat-sign-connector"; - String metadataUrl = - "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; + String metadataUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; Assertions.assertEquals(StringUtils.EMPTY, versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); Assertions.assertEquals(StringUtils.EMPTY, versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), @@ -335,26 +342,26 @@ void testGetProductJsonByVersion() { repoName = "adobe-acrobat-sign-connector"; ReflectionTestUtils.setField(versionService, "repoName", repoName); ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifact productArtifact = - new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, targetArtifactId, "iar", null, true, null); + MavenArtifact productArtifact = new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, + targetArtifactId, "iar", null, true, null); metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); metaProductArtifact.setGroupId(targetGroupId); metaProductArtifact.setArtifactId(targetArtifactId); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) - .thenReturn(null); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), + Mockito.anyString())).thenReturn(null); Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) - .thenReturn(mockContent); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), + Mockito.anyString())).thenReturn(mockContent); try { when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)) - .thenThrow(new IOException("Mock IO Exception")); + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenThrow( + new IOException("Mock IO Exception")); Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); } catch (IOException e) { Fail.fail("Mock setup should not throw an exception"); @@ -363,8 +370,7 @@ void testGetProductJsonByVersion() { @Test void testConvertMavenArtifactToModel() { - String downloadUrl = - "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; + String downloadUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; String artifactName = "Adobe Acrobat Sign Connector (iar)"; MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", @@ -408,22 +414,22 @@ void testBuildDownloadUrlFromArtifactAndVersion() { // Set up artifact for testing String targetArtifactId = "adobe-acrobat-sign-connector"; String targetGroupId = "com.axonivy.connector"; - MavenArtifact targetArtifact = - new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, null); + MavenArtifact targetArtifact = new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, + null); String targetVersion = "10.0.10"; // Assert case without archived artifact - String expectedResult = - String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, - "com/axonivy/connector", targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); + String expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, + MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, "com/axonivy/connector", targetArtifactId, targetVersion, + targetArtifactId, targetVersion, "iar"); String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); Assertions.assertEquals(expectedResult, result); // Assert case with artifact not match & use custom repo - ArchivedArtifact adobeArchivedArtifactVersion9 = - new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", "adobe-connector"); - ArchivedArtifact adobeArchivedArtifactVersion8 = - new ArchivedArtifact("10.0.8", "com.axonivy.adobe.sign.connector", "adobe-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", + "adobe-connector"); + ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.adobe.sign.connector", + "adobe-sign-connector"); archivedArtifactsMap.put(targetArtifactId, List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); String customRepoUrl = "https://nexus.axonivy.com"; targetArtifact.setRepoUrl(customRepoUrl); @@ -447,16 +453,16 @@ void testBuildDownloadUrlFromArtifactAndVersion() { void testFindArchivedArtifactInfoBestMatchWithVersion() { String targetArtifactId = "adobe-acrobat-sign-connector"; String targetVersion = "10.0.10"; - ArchivedArtifact result = - versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + ArchivedArtifact result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, + targetVersion); Assertions.assertNull(result); // Assert case with target version higher than all of latest version from // archived artifact list - ArchivedArtifact adobeArchivedArtifactVersion8 = - new ArchivedArtifact("10.0.8", "com.axonivy.connector", "adobe-sign-connector"); - ArchivedArtifact adobeArchivedArtifactVersion9 = - new ArchivedArtifact("10.0.9", "com.axonivy.connector", "adobe-acrobat-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.connector", + "adobe-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.connector", + "adobe-acrobat-sign-connector"); List archivedArtifacts = new ArrayList<>(); archivedArtifacts.add(adobeArchivedArtifactVersion8); archivedArtifacts.add(adobeArchivedArtifactVersion9); @@ -470,8 +476,8 @@ void testFindArchivedArtifactInfoBestMatchWithVersion() { Assertions.assertEquals(adobeArchivedArtifactVersion8, result); // Assert case with target version is in range of archived artifact list - ArchivedArtifact adobeArchivedArtifactVersion10 = - new ArchivedArtifact("10.0.10", "com.axonivy.connector", "adobe-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion10 = new ArchivedArtifact("10.0.10", "com.axonivy.connector", + "adobe-sign-connector"); archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); diff --git a/marketplace-service/src/test/resources/meta.json b/marketplace-service/src/test/resources/meta.json index d46c28424..6f23b2898 100644 --- a/marketplace-service/src/test/resources/meta.json +++ b/marketplace-service/src/test/resources/meta.json @@ -3,21 +3,21 @@ "id": "amazon-comprehend", "names": [ { - "locale":"en", + "locale": "en", "value": "Amazon Comprehend" }, { - "locale":"de", + "locale": "de", "value": "Amazon Comprehend DE" } ], "descriptions": [ { - "locale":"en", + "locale": "en", "value": "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data." }, { - "locale":"de", + "locale": "de", "value": "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data. DE" } ], From f317f2c0be88dacbc2ebfea6a2deea89cfb77170 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Fri, 19 Jul 2024 08:48:57 +0700 Subject: [PATCH 25/62] MARP-704: Intergrade repo marketplace-ui into marketplace repo Remove unnecessary file --- marketplace-ui/.github/workflows/ci-build.yml | 57 ----- .../.github/workflows/dev-build.yml | 28 --- marketplace-ui/LICENSE | 201 ------------------ marketplace-ui/SECURITY.md | 25 --- 4 files changed, 311 deletions(-) delete mode 100644 marketplace-ui/.github/workflows/ci-build.yml delete mode 100644 marketplace-ui/.github/workflows/dev-build.yml delete mode 100644 marketplace-ui/LICENSE delete mode 100644 marketplace-ui/SECURITY.md diff --git a/marketplace-ui/.github/workflows/ci-build.yml b/marketplace-ui/.github/workflows/ci-build.yml deleted file mode 100644 index 8a437d1f4..000000000 --- a/marketplace-ui/.github/workflows/ci-build.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: CI Build -run-name: Build on branch ${{github.ref_name}} triggered by ${{github.actor}} - -on: - push: - branches-ignore: - - develop - - master - workflow_dispatch: - -jobs: - build: - name: Build - runs-on: self-hosted - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install Dependencies - run: npm install - - name: Build project - run: npm run build - - analysis: - name: Sonarqube - needs: build - runs-on: self-hosted - env: - SONAR_PROJECT_KEY: 'AxonIvy-Market-UI' - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - steps: - - name: Execute Tests - run: npm run test - - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ env.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} - with: - args: - -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} - - name: SonarQube Quality Gate check - id: sonarqube-quality-gate-check - uses: sonarsource/sonarqube-quality-gate-action@master - timeout-minutes: 5 - env: - SONAR_TOKEN: ${{ env.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} - - - name: Clean up - run: | - rm -rf * diff --git a/marketplace-ui/.github/workflows/dev-build.yml b/marketplace-ui/.github/workflows/dev-build.yml deleted file mode 100644 index cb34d8665..000000000 --- a/marketplace-ui/.github/workflows/dev-build.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Dev Build -run-name: Build and Deploy Marketplace-UI on branch ${{github.ref_name}} by ${{github.actor}} - -on: - push: - branches: [ "develop" ] - workflow_dispatch: - -jobs: - build: - name: Build and deploy new code to Deployment directory - runs-on: self-hosted - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - - name: Install Dependencies - run: npm install - - name: Build Angular app - run: npm run build -- --configuration production --output-path=dist - - name: Execute Tests - run: npm run test - - name: Copy files to Deployment directory - if: success() - run: sudo cp -r dist/* /var/www/marketplace-ui diff --git a/marketplace-ui/LICENSE b/marketplace-ui/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/marketplace-ui/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed 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. diff --git a/marketplace-ui/SECURITY.md b/marketplace-ui/SECURITY.md deleted file mode 100644 index 1d4c06f71..000000000 --- a/marketplace-ui/SECURITY.md +++ /dev/null @@ -1,25 +0,0 @@ -## Reporting a Vulnerability - -At Axon Ivy, we take security seriously. If you believe you've found a security vulnerability in our software, we encourage you to let us know right away. We investigate all reported vulnerabilities promptly. - -To report a vulnerability, please send an email to [security@axonivy.com](mailto:security@axonivy.com) with the following information: - -- Description of the vulnerability -- Steps to reproduce the vulnerability -- Any additional information or context that may be helpful - -Please refrain from publicly disclosing the vulnerability until it has been addressed by our team. - -## Response Time - -We strive to respond to security vulnerability reports as quickly as possible. Upon receiving your report, we will acknowledge it within 72 hours and we will release a patch as soon as possible depending on complexity, but historically within a few days. -Please report (suspected) security vulnerabilities at https://support.axonivy.com/. - - -## Responsible Disclosure - -We encourage responsible disclosure of security vulnerabilities. We believe that working together with security researchers and the broader community helps us improve the security of our software for everyone. - -## Contact - -For any questions or concerns regarding security, please contact us at [security@axonivy.com](mailto:security@axonivy.com). From 118e4ba80445747fc0414633263511d7d659efac Mon Sep 17 00:00:00 2001 From: Tu Thanh Nguyen <138571181+tutn-axonivy@users.noreply.github.com> Date: Fri, 19 Jul 2024 09:13:15 +0700 Subject: [PATCH 26/62] MARP-558 multilingualism for detail page description (#34) --- .../market/constants/GitHubConstants.java | 1 + .../market/constants/ReadmeConstants.java | 1 + .../com/axonivy/market/entity/Product.java | 28 ++-- .../market/entity/ProductModuleContent.java | 3 +- .../market/factory/ProductFactory.java | 44 +++---- .../impl/GHAxonIvyMarketRepoServiceImpl.java | 2 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 82 ++++++++---- .../market/model/MultilingualismValue.java | 17 --- .../axonivy/market/model/ProductModel.java | 19 +-- .../service/impl/ProductServiceImpl.java | 66 +++++----- .../controller/ProductControllerTest.java | 49 +++---- .../ProductDetailsControllerTest.java | 46 ++++--- .../market/factory/ProductFactoryTest.java | 18 ++- .../GHAxonIvyProductRepoServiceImplTest.java | 52 ++++---- .../service/ProductServiceImplTest.java | 120 +++++++++--------- .../service/VersionServiceImplTest.java | 4 + 16 files changed, 303 insertions(+), 249 deletions(-) delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java index 39d35a77c..6e81a25f9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -12,6 +12,7 @@ public class GitHubConstants { public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; public static final String GITHUB_PROVIDER_NAME = "GitHub"; public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; + public static final String README_FILE_LOCALE_REGEX = "_(..)"; @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class Json { diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java index 6d3024e9f..2f4e4f946 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java @@ -7,6 +7,7 @@ public class ReadmeConstants { public static final String IMAGES = "images"; public static final String README_FILE = "README.md"; + public static final String README_FILE_NAME = "README"; public static final String DEMO_PART = "## Demo"; public static final String SETUP_PART = "## Setup"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index a6d4c39df..ea1964863 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -1,23 +1,25 @@ package com.axonivy.market.entity; +import static com.axonivy.market.constants.EntityConstants.PRODUCT; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.io.Serializable; -import java.util.Date; -import java.util.List; - -import static com.axonivy.market.constants.EntityConstants.PRODUCT; @Getter @Setter @@ -31,10 +33,10 @@ public class Product implements Serializable { private String id; private String marketDirectory; @JsonProperty - private MultilingualismValue names; + private Map names; private String version; @JsonProperty - private MultilingualismValue shortDescriptions; + private Map shortDescriptions; private String logoUrl; private Boolean listed; private String type; diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java index d2b6d1145..f8df6160c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -6,6 +6,7 @@ import lombok.Setter; import java.io.Serializable; +import java.util.Map; @Getter @Setter @@ -14,7 +15,7 @@ public class ProductModuleContent implements Serializable { private static final long serialVersionUID = 1L; private String tag; - private String description; + private Map description; private String setup; private String demo; private Boolean isDependency; diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index b7c4580ec..034f26594 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -1,29 +1,31 @@ package com.axonivy.market.factory; +import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_NAME; +import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_URL; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.github.GHContent; +import org.springframework.util.CollectionUtils; + import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.Language; import com.axonivy.market.github.model.Meta; import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.DisplayValue; -import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.kohsuke.github.GHContent; -import org.springframework.util.CollectionUtils; - -import java.io.IOException; -import java.util.List; - -import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_NAME; -import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_URL; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.apache.commons.lang3.StringUtils.EMPTY; @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -76,15 +78,11 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent return product; } - private static MultilingualismValue mappingMultilingualismValueByMetaJSONFile(List list) { - MultilingualismValue value = new MultilingualismValue(); + private static Map mappingMultilingualismValueByMetaJSONFile(List list) { + Map value = new HashMap<>(); if (!CollectionUtils.isEmpty(list)) { for (DisplayValue name : list) { - if (Language.EN.getValue().equalsIgnoreCase(name.getLocale())) { - value.setEn(name.getValue()); - } else if (Language.DE.getValue().equalsIgnoreCase(name.getLocale())) { - value.setDe(name.getValue()); - } + value.put(name.getLocale(), name.getValue()); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index 1d3f286eb..c44a047bc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -133,4 +133,4 @@ public GHRepository getRepository() { return repository; } -} +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index 4c6516584..033503792 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -1,5 +1,24 @@ package com.axonivy.market.github.service.impl; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; @@ -8,31 +27,15 @@ import com.axonivy.market.constants.ReadmeConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.Language; import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHTag; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import lombok.extern.log4j.Log4j2; @Log4j2 @Service @@ -150,14 +153,17 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, List contents = getProductFolderContents(product, ghRepository, tag); productModuleContent.setTag(tag); getDependencyContentsFromProductJson(productModuleContent, contents); - GHContent readmeFile = contents.stream().filter(GHContent::isFile) - .filter(content -> ReadmeConstants.README_FILE.equals(content.getName())).findFirst().orElse(null); - if (Objects.nonNull(readmeFile)) { - String readmeContents = new String(readmeFile.read().readAllBytes()); - if (hasImageDirectives(readmeContents)) { - readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); + List readmeFiles = contents.stream().filter(GHContent::isFile) + .filter(content -> content.getName().startsWith(ReadmeConstants.README_FILE_NAME)).toList(); + if (!CollectionUtils.isEmpty(readmeFiles)) { + for (GHContent readmeFile : readmeFiles) { + String readmeContents = new String(readmeFile.read().readAllBytes()); + if (hasImageDirectives(readmeContents)) { + readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); + } + String locale = getReadmeFileLocale(readmeFile.getName()); + getExtractedPartsOfReadme(productModuleContent, readmeContents, locale); } - getExtractedPartsOfReadme(productModuleContent, readmeContents); } } catch (Exception e) { log.error("Cannot get product.json and README file's content {}", e); @@ -166,6 +172,16 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, return productModuleContent; } + private String getReadmeFileLocale(String readmeFile) { + String result = StringUtils.EMPTY; + Pattern pattern = Pattern.compile(GitHubConstants.README_FILE_LOCALE_REGEX); + Matcher matcher = pattern.matcher(readmeFile); + if (matcher.find()) { + result = matcher.group(1); + } + return result; + } + private void getDependencyContentsFromProductJson(ProductModuleContent productModuleContent, List contents) throws IOException { GHContent productJsonFile = getProductJsonFile(contents); @@ -223,7 +239,8 @@ private void getImagesFromImageFolder(Product product, List contents, // Cover some cases including when demo and setup parts switch positions or // missing one of them - public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, String readmeContents) { + public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, String readmeContents, + String locale) { String[] parts = readmeContents.split(DEMO_SETUP_TITLE); int demoIndex = readmeContents.indexOf(ReadmeConstants.DEMO_PART); int setupIndex = readmeContents.indexOf(ReadmeConstants.SETUP_PART); @@ -249,11 +266,22 @@ public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, setup = parts[1]; } - productModuleContent.setDescription(description.trim()); + setDescriptionWithLocale(productModuleContent, description.trim(), locale); productModuleContent.setDemo(demo.trim()); productModuleContent.setSetup(setup.trim()); } + private void setDescriptionWithLocale(ProductModuleContent productModuleContent, String description, String locale) { + if (productModuleContent.getDescription() == null) { + productModuleContent.setDescription(new HashMap<>()); + } + if (StringUtils.isEmpty(locale)) { + productModuleContent.getDescription().put(Language.EN.getValue(), description); + } else { + productModuleContent.getDescription().put(locale.toLowerCase(), description); + } + } + private List getProductFolderContents(Product product, GHRepository ghRepository, String tag) throws IOException { String productFolderPath = ghRepository.getDirectoryContent(CommonConstants.SLASH, tag).stream() diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java deleted file mode 100644 index 389c4832e..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.axonivy.market.model; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.io.Serializable; - -@Getter -@Setter -@NoArgsConstructor -public class MultilingualismValue implements Serializable { - private static final long serialVersionUID = -4193508237020296419L; - - private String en; - private String de; -} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java index 79ea5b5bf..fba99e845 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java @@ -1,16 +1,19 @@ package com.axonivy.market.model; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import java.util.List; +import java.util.Map; + import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.core.Relation; -import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Getter @Setter @@ -19,8 +22,8 @@ @JsonInclude(Include.NON_NULL) public class ProductModel extends RepresentationModel { private String id; - private MultilingualismValue names; - private MultilingualismValue shortDescriptions; + private Map names; + private Map shortDescriptions; private String logoUrl; private String type; private List tags; diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 4a8afd880..e87bf73ef 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -1,5 +1,37 @@ package com.axonivy.market.service.impl; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.GitHubRepoMeta; @@ -19,38 +51,8 @@ import com.axonivy.market.service.ProductService; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHTag; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Random; - -import static java.util.Optional.ofNullable; -import static org.apache.commons.lang3.StringUtils.EMPTY; +import lombok.extern.log4j.Log4j2; @Log4j2 @Service @@ -70,7 +72,7 @@ public class ProductServiceImpl implements ProductService { private String installationCountPath; public static final String NON_NUMERIC_CHAR = "[^0-9.]"; - private final Random random = new Random(); + private final SecureRandom random = new SecureRandom(); public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService) { diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 079526960..fa5c2ff4e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -1,12 +1,14 @@ package com.axonivy.market.controller; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.service.ProductService; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,16 +25,17 @@ import org.springframework.hateoas.PagedModel.PageMetadata; import org.springframework.http.HttpStatus; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.enums.Language; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.ProductService; @ExtendWith(MockitoExtension.class) class ProductControllerTest { - private static final String PRODUCT_ID_SAMPLE = "amazon-comprehend"; private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; private static final String PRODUCT_NAME_DE_SAMPLE = "Amazon Comprehend DE"; private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; @@ -85,8 +88,10 @@ void testFindProducts() { assertEquals(HttpStatus.OK, result.getStatusCode()); assertTrue(result.hasBody()); assertEquals(1, result.getBody().getContent().size()); - assertEquals(PRODUCT_NAME_SAMPLE, result.getBody().getContent().iterator().next().getNames().getEn()); - assertEquals(PRODUCT_NAME_DE_SAMPLE, result.getBody().getContent().iterator().next().getNames().getDe()); + assertEquals(PRODUCT_NAME_SAMPLE, + result.getBody().getContent().iterator().next().getNames().get(Language.EN.getValue())); + assertEquals(PRODUCT_NAME_DE_SAMPLE, + result.getBody().getContent().iterator().next().getNames().get(Language.DE.getValue())); } @Test @@ -100,13 +105,13 @@ void testSyncProducts() { private Product createProductMock() { Product mockProduct = new Product(); mockProduct.setId("amazon-comprehend"); - MultilingualismValue name = new MultilingualismValue(); - name.setEn(PRODUCT_NAME_SAMPLE); - name.setDe(PRODUCT_NAME_DE_SAMPLE); + Map name = new HashMap<>(); + name.put(Language.EN.getValue(), PRODUCT_NAME_SAMPLE); + name.put(Language.DE.getValue(), PRODUCT_NAME_DE_SAMPLE); mockProduct.setNames(name); - MultilingualismValue shortDescription = new MultilingualismValue(); - shortDescription.setEn(PRODUCT_DESC_SAMPLE); - shortDescription.setDe(PRODUCT_DESC_DE_SAMPLE); + Map shortDescription = new HashMap<>(); + shortDescription.put(Language.EN.getValue(), PRODUCT_DESC_SAMPLE); + shortDescription.put(Language.DE.getValue(), PRODUCT_DESC_DE_SAMPLE); mockProduct.setShortDescriptions(shortDescription); mockProduct.setType("connector"); mockProduct.setTags(List.of("AI")); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 3551a82cf..e1163587e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -1,12 +1,18 @@ package com.axonivy.market.controller; -import com.axonivy.market.assembler.ProductDetailModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.service.ProductService; -import com.axonivy.market.service.VersionService; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Objects; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,13 +23,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import java.util.List; -import java.util.Objects; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { @@ -100,9 +106,9 @@ void testSyncInstallationCount() { private Product mockProduct() { Product mockProduct = new Product(); mockProduct.setId(DOCKER_CONNECTOR_ID); - MultilingualismValue name = new MultilingualismValue(); - name.setEn(PRODUCT_NAME_SAMPLE); - name.setDe(PRODUCT_NAME_DE_SAMPLE); + Map name = new HashMap<>(); + name.put(Language.EN.getValue(), PRODUCT_NAME_SAMPLE); + name.put(Language.DE.getValue(), PRODUCT_NAME_DE_SAMPLE); mockProduct.setNames(name); mockProduct.setLanguage("English"); return mockProduct; @@ -111,9 +117,9 @@ private Product mockProduct() { private ProductDetailModel createProductMockWithDetails() { ProductDetailModel mockProductDetail = new ProductDetailModel(); mockProductDetail.setId(DOCKER_CONNECTOR_ID); - MultilingualismValue name = new MultilingualismValue(); - name.setEn(PRODUCT_NAME_SAMPLE); - name.setDe(PRODUCT_NAME_DE_SAMPLE); + Map name = new HashMap<>(); + name.put(Language.EN.getValue(), PRODUCT_NAME_SAMPLE); + name.put(Language.DE.getValue(), PRODUCT_NAME_DE_SAMPLE); mockProductDetail.setNames(name); mockProductDetail.setType("connector"); mockProductDetail.setCompatibility("10.0+"); diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index 2446c18c1..5b9f3727e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -19,6 +19,20 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.IOException; +import java.io.InputStream; + +import com.axonivy.market.github.model.Meta; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; + @ExtendWith(MockitoExtension.class) class ProductFactoryTest { private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; @@ -34,8 +48,8 @@ void testMappingByGHContent() throws IOException { when(mockContent.read()).thenReturn(inputStream); result = ProductFactory.mappingByGHContent(product, mockContent); assertNotEquals(null, result); - assertEquals("Amazon Comprehend", result.getNames().getEn()); - assertEquals("Amazon Comprehend DE", result.getNames().getDe()); + assertEquals("Amazon Comprehend", result.getNames().get(Language.EN.getValue())); + assertEquals("Amazon Comprehend DE", result.getNames().get(Language.DE.getValue())); } @Test diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java index 1c7bdf164..52c01e218 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java @@ -1,13 +1,22 @@ package com.axonivy.market.service; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.constants.ReadmeConstants; -import com.axonivy.market.entity.Product; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; -import com.fasterxml.jackson.databind.JsonNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,22 +31,15 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; @ExtendWith(MockitoExtension.class) class GHAxonIvyProductRepoServiceImplTest { @@ -214,7 +216,7 @@ void testGetReadmeAndProductContentsFromTag() throws IOException { assertEquals("com.axonivy.utils.bpmnstatistic", result.getGroupId()); assertEquals("bpmn-statistic", result.getArtifactId()); assertEquals("iar", result.getType()); - assertEquals("Test README", result.getDescription()); + assertEquals("Test README", result.getDescription().get(Language.EN.getValue())); assertEquals("Demo content", result.getDemo()); assertEquals("Setup content (https://raw.githubusercontent.com/image.png)", result.getSetup()); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index aa02a2c9f..1242e442f 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -1,21 +1,33 @@ package com.axonivy.market.service; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.GitHubRepoMeta; -import com.axonivy.market.entity.Product; -import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.enums.FileStatus; -import com.axonivy.market.enums.FileType; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.github.model.GitHubFile; -import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.repository.GitHubRepoMetaRepository; -import com.axonivy.market.repository.ProductRepository; -import com.axonivy.market.service.impl.ProductServiceImpl; +import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,33 +50,22 @@ import org.springframework.data.domain.Sort; import org.springframework.test.util.ReflectionTestUtils; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.FileStatus; +import com.axonivy.market.enums.FileType; +import com.axonivy.market.enums.Language; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.github.model.GitHubFile; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.repository.GitHubRepoMetaRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.impl.ProductServiceImpl; @ExtendWith(MockitoExtension.class) class ProductServiceImplTest { @@ -249,25 +250,26 @@ void testFindAllProductsWithKeyword() throws IOException { verify(productRepository).findAll(any(Pageable.class)); // Test has keyword - when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(), any(Pageable.class))).thenReturn( - new PageImpl<>( - mockResultReturn.stream().filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME)) - .collect(Collectors.toList()))); + when(productRepository.searchByNameOrShortDescriptionRegex(any(), any(), any(Pageable.class))) + .thenReturn(new PageImpl<>(mockResultReturn.stream() + .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME)) + .collect(Collectors.toList()))); // Executes result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); verify(productRepository).findAll(any(Pageable.class)); assertTrue(result.hasContent()); - assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); + assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().get(Language.EN.getValue())); // Test has keyword and type is connector - when(productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))).thenReturn(new PageImpl<>( - mockResultReturn.stream().filter( - product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) && product.getType() - .equals(TypeOption.CONNECTORS.getCode())).collect(Collectors.toList()))); + when(productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))) + .thenReturn(new PageImpl<>(mockResultReturn.stream() + .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME) + && product.getType().equals(TypeOption.CONNECTORS.getCode())) + .collect(Collectors.toList()))); // Executes result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); assertTrue(result.hasContent()); - assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); + assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().get(Language.EN.getValue())); } @Test @@ -360,18 +362,18 @@ void testGetCompatibilityFromNumericTag() { private Page createPageProductsMock() { var mockProducts = new ArrayList(); - MultilingualismValue name = new MultilingualismValue(); + Map name = new HashMap<>(); Product mockProduct = new Product(); mockProduct.setId(SAMPLE_PRODUCT_ID); - name.setEn(SAMPLE_PRODUCT_NAME); + name.put(Language.EN.getValue(), SAMPLE_PRODUCT_NAME); mockProduct.setNames(name); mockProduct.setType("connector"); mockProducts.add(mockProduct); mockProduct = new Product(); mockProduct.setId("tel-search-ch-connector"); - name = new MultilingualismValue(); - name.setEn("Swiss phone directory"); + name = new HashMap<>(); + name.put(Language.EN.getValue(), "Swiss phone directory"); mockProduct.setNames(name); mockProduct.setType("util"); mockProducts.add(mockProduct); @@ -403,7 +405,9 @@ private ProductModuleContent mockReadmeProductContent() { ProductModuleContent productModuleContent = new ProductModuleContent(); productModuleContent.setTag("v10.0.2"); productModuleContent.setName("Amazon Comprehend"); - productModuleContent.setDescription("testDescription"); + Map description = new HashMap<>(); + description.put(Language.EN.getValue(), "testDescription"); + productModuleContent.setDescription(description); return productModuleContent; } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java index 60ffb9248..a5e98a854 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java @@ -235,6 +235,10 @@ void testGetVersionsFromArtifactDetails() { versionFromArtifact.add("10.0.19"); versionFromArtifact.add("10.0.20"); versionFromArtifact.add("10.0.21"); + versionFromArtifact.add("10.0.22"); + versionFromArtifact.add("10.0.23"); + versionFromArtifact.add("10.0.24"); + versionFromArtifact.add("10.0.25"); try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); From 1cfec911bf5b0fec840feb2d34e981b50f1fe304 Mon Sep 17 00:00:00 2001 From: Khanh Nguyen Date: Fri, 19 Jul 2024 10:04:15 +0700 Subject: [PATCH 27/62] feature/MARP-357-Adjust githubAuthCallbackUrl --- marketplace-ui/src/environments/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/marketplace-ui/src/environments/environment.ts b/marketplace-ui/src/environments/environment.ts index ed5e76440..77ad2feba 100644 --- a/marketplace-ui/src/environments/environment.ts +++ b/marketplace-ui/src/environments/environment.ts @@ -2,6 +2,6 @@ export const environment = { production: true, apiUrl: 'http://10.193.8.78:9090/marketplace-service', githubClientId: 'Ov23liVMliBxBqdQ7FnG', - githubAuthCallbackUrl: 'http://10.193.8.78:4200/auth/github/callback', + githubAuthCallbackUrl: 'http://marketplace.server.ivy-cloud.com:4200/auth/github/callback', dayInMiliseconds: 86400000 }; From b5f0afcdfe153ba0f35a0ed8d179224af08b4232 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:51:55 +0700 Subject: [PATCH 28/62] bugfix/MARP-690-Feedback-Anais-1-space-on-top-of-the-landing-page --- marketplace-ui/src/app/modules/product/product.component.html | 2 +- marketplace-ui/src/app/modules/product/product.component.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/marketplace-ui/src/app/modules/product/product.component.html b/marketplace-ui/src/app/modules/product/product.component.html index 3a5949fed..333bbcbf6 100644 --- a/marketplace-ui/src/app/modules/product/product.component.html +++ b/marketplace-ui/src/app/modules/product/product.component.html @@ -1,5 +1,5 @@
-
+

{{ translateService.get('common.branch') | async }}

diff --git a/marketplace-ui/src/app/modules/product/product.component.scss b/marketplace-ui/src/app/modules/product/product.component.scss index 47ff283db..e01ebbe71 100644 --- a/marketplace-ui/src/app/modules/product/product.component.scss +++ b/marketplace-ui/src/app/modules/product/product.component.scss @@ -1,3 +1,7 @@ .product-container { min-height: 6em; } + +.mt-8{ + margin-top: 8rem; +} From a411c5644f4f8300a3f4a7f7d6d5511a786dad3e Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:59:55 +0700 Subject: [PATCH 29/62] Bugfix/marp 692 feedback anais 3 adjustments for footer --- .../shared/components/footer/footer.component.html | 4 ++-- .../shared/components/footer/footer.component.scss | 11 +++++++++++ marketplace-ui/src/assets/i18n/de.yaml | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.html b/marketplace-ui/src/app/shared/components/footer/footer.component.html index 9f215e2ba..f9a831c6d 100644 --- a/marketplace-ui/src/app/shared/components/footer/footer.component.html +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.html @@ -52,14 +52,14 @@
+ class="d-flex flex-lg-row flex-column justify-content-between py-4">