diff --git a/.github/changelog.yaml b/.github/changelog.yaml new file mode 100644 index 0000000..0a5526e --- /dev/null +++ b/.github/changelog.yaml @@ -0,0 +1,13 @@ +sections: + - title: Major changes + labels: + - "release/super-feature" + - title: Complete changelog + labels: + - "bug" + - "enhancement" + - "dependencies" +template: | + {{ range $section := .Sections }}{{ if $section.Items }}### {{ $section.GetTitle }}{{ range $item := $section.Items }} + * [#{{ $item.GetID }}]({{ $item.GetURL }}) - {{ $item.GetTitle }}{{ end }}{{ end }} + {{ end }} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..64412da --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: maven + directory: "/" + schedule: + interval: daily + labels: + - dependencies \ No newline at end of file diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml new file mode 100644 index 0000000..7270213 --- /dev/null +++ b/.github/workflows/build-branch.yml @@ -0,0 +1,13 @@ +name: Build Feature Branch + +on: + push: + branches: + - '**' + - '!main' + - '!fix/[0-9]+.[0-9]+.x' + +jobs: + branch: + uses: onecx/ci-quarkus/.github/workflows/build-branch.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 0000000..acff155 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,9 @@ +name: Build Pull Request + +on: + pull_request: + +jobs: + pr: + uses: onecx/ci-quarkus/.github/workflows/build-pr.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..b04a59a --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,9 @@ +name: Build Release +on: + push: + tags: + - '**' +jobs: + release: + uses: onecx/ci-quarkus/.github/workflows/build-release.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..53b0ddd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: + - 'main' + - 'fix/[0-9]+.[0-9]+.x' + paths-ignore: + - '.gitignore' + - 'CODEOWNERS' + - 'LICENSE' + - '*.md' + - '*.adoc' + - '*.txt' + - '.all-contributorsrc' + +jobs: + build: + uses: onecx/ci-quarkus/.github/workflows/build.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/create-fix-branch.yml b/.github/workflows/create-fix-branch.yml new file mode 100644 index 0000000..92af624 --- /dev/null +++ b/.github/workflows/create-fix-branch.yml @@ -0,0 +1,7 @@ +name: Create Fix Branch +on: + workflow_dispatch: +jobs: + fix: + uses: onecx/ci-common/.github/workflows/create-fix-branch.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..c97eb42 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,7 @@ +name: Create Release Version +on: + workflow_dispatch: +jobs: + release: + uses: onecx/ci-common/.github/workflows/create-release.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..e43d7dc --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,10 @@ +name: Update documentation +on: + push: + branches: [ main ] + paths: + - 'docs/**' +jobs: + release: + uses: onecx/ci-common/.github/workflows/documentation.yml@v1 + secrets: inherit diff --git a/.github/workflows/sonar-pr.yml b/.github/workflows/sonar-pr.yml new file mode 100644 index 0000000..02c3e1e --- /dev/null +++ b/.github/workflows/sonar-pr.yml @@ -0,0 +1,12 @@ +name: Sonar Pull Request + +on: + workflow_run: + workflows: ["Build Pull Request"] + types: + - completed + +jobs: + pr: + uses: onecx/ci-quarkus/.github/workflows/quarkus-pr-sonar.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c7863e --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c5cfd8f --- /dev/null +++ b/pom.xml @@ -0,0 +1,243 @@ + + + + 4.0.0 + + + io.github.onecx + onecx-quarkus3-parent + 0.23.0 + + + onecx-workspace-svc + 999-SNAPSHOT + + + + + org.tkit.quarkus.lib + tkit-quarkus-data-import + + + org.tkit.quarkus.lib + tkit-quarkus-jpa + + + org.tkit.quarkus.lib + tkit-quarkus-log-cdi + + + org.tkit.quarkus.lib + tkit-quarkus-log-rs + + + org.tkit.quarkus.lib + tkit-quarkus-log-json + + + org.tkit.quarkus.lib + tkit-quarkus-rest + + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-liquibase + + + com.github.blagerweij + liquibase-sessionlock + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-opentelemetry + + + + + org.projectlombok + lombok + + + org.mapstruct + mapstruct + + + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit5-mockito + test + + + io.rest-assured + rest-assured + test + + + org.tkit.quarkus.lib + tkit-quarkus-test-db-import + test + + + io.quarkus + quarkus-test-keycloak-server + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + + jaxrs-spec + DTO + false + false + false + false + false + true + quarkus + + / + false + true + true + true + true + true + java8 + true + true + false + true + + + + + internal + + generate + + + src/main/openapi/onecx-workspace-internal-openapi.yaml + gen.io.github.onecx.workspace.rs.internal + gen.io.github.onecx.workspace.rs.internal.model + DTO + + + + legacy + + generate + + + src/main/openapi/onecx-workspace-legacy-openapi.yaml + gen.io.github.onecx.workspace.rs.legacy + gen.io.github.onecx.workspace.rs.legacy.model + DTO + + + + + di-workspace-v1 + + generate + + + src/main/openapi/di-workspace-v1.yaml + gen.io.github.onecx.workspace.di.workspace.v1 + gen.io.github.onecx.workspace.di.workspace.v1.model + DTOV1 + true + false + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire-plugin.version} + + + interpolated5 + interpolated6 + interpolated7 + interpolated8 + + + + + + + diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..1963f14 --- /dev/null +++ b/src/main/docker/Dockerfile.jvm @@ -0,0 +1,14 @@ +FROM registry.access.redhat.com/ubi9/openjdk-17:1.15 + +ENV LANGUAGE='en_US:en' + +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 + +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" \ No newline at end of file diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..c97f217 --- /dev/null +++ b/src/main/docker/Dockerfile.native @@ -0,0 +1,11 @@ +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.2 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/src/main/helm/Chart.yaml b/src/main/helm/Chart.yaml new file mode 100644 index 0000000..0e97809 --- /dev/null +++ b/src/main/helm/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +name: onecx-workspace-svc +version: 0.0.0 +appVersion: 0.0.0 +description: Onecx workspace service +keywords: + - tenant +sources: + - https://github.com/onecx/onecx-workspace-svc +maintainers: + - name: Tkit Developer + email: tkit_dev@1000kit.org +dependencies: + - name: helm-quarkus-app + alias: app + version: ^0 + repository: oci://ghcr.io/onecx/charts \ No newline at end of file diff --git a/src/main/helm/values.yaml b/src/main/helm/values.yaml new file mode 100644 index 0000000..51c5784 --- /dev/null +++ b/src/main/helm/values.yaml @@ -0,0 +1,6 @@ +app: + image: + repository: "onecx/onecx-workspace-svc" + tag: 999-SNAPSHOT + db: + enabled: true \ No newline at end of file diff --git a/src/main/java/io/github/onecx/workspace/domain/criteria/WorkspaceSearchCriteria.java b/src/main/java/io/github/onecx/workspace/domain/criteria/WorkspaceSearchCriteria.java new file mode 100644 index 0000000..8948e8a --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/criteria/WorkspaceSearchCriteria.java @@ -0,0 +1,17 @@ +package io.github.onecx.workspace.domain.criteria; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WorkspaceSearchCriteria { + + private String workspaceName; + + private String themeName; + + private Integer pageNumber; + + private Integer pageSize; +} \ No newline at end of file diff --git a/src/main/java/io/github/onecx/workspace/domain/daos/MenuItemDAO.java b/src/main/java/io/github/onecx/workspace/domain/daos/MenuItemDAO.java new file mode 100644 index 0000000..69f1955 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/daos/MenuItemDAO.java @@ -0,0 +1,180 @@ +package io.github.onecx.workspace.domain.daos; + +import static io.github.onecx.workspace.domain.models.MenuItem.MENU_ITEM_WORKSPACE_AND_TRANSLATIONS; +import static jakarta.persistence.criteria.JoinType.LEFT; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.criteria.Join; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.models.TraceableEntity_; + +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.domain.models.MenuItem_; +import io.github.onecx.workspace.domain.models.Workspace; +import io.github.onecx.workspace.domain.models.Workspace_; + +@ApplicationScoped +public class MenuItemDAO extends AbstractDAO { + + /** + * This method updates menu items with new workspaceName provided as a param + * based on oldworkspaceName provided as a param + */ + @Transactional + public void updateMenuItems(String newWorkspaceName, String oldWorkspaceName, String baseUrl) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var update = cb.createCriteriaUpdate(MenuItem.class); + var root = update.from(MenuItem.class); + update.set(MenuItem_.WORKSPACE_NAME, newWorkspaceName); + update.set(MenuItem_.URL, baseUrl); + var subquery = update.subquery(MenuItem.class); + var root2 = subquery.from(MenuItem.class); + subquery.select(root2); + + Join join = root2.join(MenuItem_.WORKSPACE, LEFT); + subquery.where(cb.equal(join.get(Workspace_.WORKSPACE_NAME), oldWorkspaceName)); + + update.where(root.in(subquery)); + this.em.createQuery(update).executeUpdate(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_UPDATE_MENU_ITEMS, ex); + } + } + + /** + * This method delete all menu items by workspace id. + * + * @param id - workspace id + */ + @Transactional + public void deleteAllMenuItemsByWorkspaceId(String id) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = this.criteriaQuery(); + var root = cq.from(MenuItem.class); + + cq.where(cb.and( + cb.equal(root.get(MenuItem_.WORKSPACE).get(TraceableEntity_.ID), id), + cb.isNull(root.get(MenuItem_.PARENT)))); + + var items = getEntityManager().createQuery(cq).getResultList(); + delete(items); + + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_DELETE_ALL_MENU_ITEMS_BY_WORKSPACE_ID, ex); + } + } + + /** + * This method delete all menu items by workspace name and application id. + * + * @param workspaceName - workspace name + * @param appId - application id + */ + @Transactional(value = Transactional.TxType.REQUIRED, rollbackOn = DAOException.class) + public void deleteAllMenuItemsByWorkspaceNameAndAppId(String workspaceName, String appId) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = this.criteriaQuery(); + var root = cq.from(MenuItem.class); + + cq.where(cb.and( + cb.equal(root.get(MenuItem_.WORKSPACE_NAME), workspaceName), + cb.equal(root.get(MenuItem_.APPLICATION_ID), appId))); + + var items = getEntityManager().createQuery(cq).getResultList(); + delete(items); + + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_DELETE_ALL_MENU_ITEMS_BY_WORKSPACE_NAME_AND_APP_ID, ex); + } + } + + /** + * This method fetches all menuItems assigned to a workspace with + * + * @param workspaceName - provided as a param and + * + * @return List of the menu items + */ + public MenuItem loadMenuItemByWorkspaceAndKey(String workspaceName, String itemKey) { + + try { + var cb = getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(MenuItem.class); + var root = cq.from(MenuItem.class); + + cq.where(cb.and( + cb.equal(root.get(MenuItem_.WORKSPACE_NAME), workspaceName), + cb.equal(root.get(MenuItem_.key), itemKey))); + + return getEntityManager() + .createQuery(cq) + .getSingleResult(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_NAME, ex); + } + } + + /** + * This method fetches all menuItems assigned to a workspace with + * + * @param workspaceName - provided as a param and + * + * @return List of the menu items + */ + public List loadAllMenuItemsByWorkspaceName(String workspaceName) { + + try { + var cb = getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(MenuItem.class); + var root = cq.from(MenuItem.class); + + cq.where(cb.equal(root.get(MenuItem_.WORKSPACE).get(Workspace_.WORKSPACE_NAME), workspaceName)); + + return getEntityManager() + .createQuery(cq) + .setHint(HINT_LOAD_GRAPH, this.getEntityManager().getEntityGraph(MENU_ITEM_WORKSPACE_AND_TRANSLATIONS)) + .getResultList(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_NAME, ex); + } + } + + /** + * This method fetches all menuItems assigned to a workspace with just + * id provided as a param + * + * @return List of the menu items + */ + public List loadAllMenuItemsByWorkspaceId(String workspaceId) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(MenuItem.class); + var menuItem = cq.from(MenuItem.class); + cq.where(cb.equal(menuItem.get(MenuItem_.WORKSPACE).get(TraceableEntity_.ID), workspaceId)); + var menuItemsQuery = getEntityManager().createQuery(cq); + menuItemsQuery.setHint(HINT_LOAD_GRAPH, + this.getEntityManager().getEntityGraph(MENU_ITEM_WORKSPACE_AND_TRANSLATIONS)); + return menuItemsQuery.getResultList(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_ID, ex); + } + } + + public enum ErrorKeys { + ERROR_UPDATE_MENU_ITEMS, + ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_ID, + ERROR_DELETE_ALL_MENU_ITEMS_BY_WORKSPACE_ID, + ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_NAME, + ERROR_LOAD_ALL_MENU_ITEM_BY_KEY, + + ERROR_DELETE_ALL_MENU_ITEMS_BY_WORKSPACE_NAME_AND_APP_ID, + } +} diff --git a/src/main/java/io/github/onecx/workspace/domain/daos/ProductDAO.java b/src/main/java/io/github/onecx/workspace/domain/daos/ProductDAO.java new file mode 100644 index 0000000..72c0c88 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/daos/ProductDAO.java @@ -0,0 +1,47 @@ +package io.github.onecx.workspace.domain.daos; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import io.github.onecx.workspace.domain.models.Product; +import io.github.onecx.workspace.domain.models.Product_; +import io.github.onecx.workspace.domain.models.Workspace_; + +@ApplicationScoped +public class ProductDAO extends AbstractDAO { + + public List getProductsForWorkspaceId(String id) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Product.class); + var root = cq.from(Product.class); + + cq.where(cb.equal(root.get(Product_.WORKSPACE).get(Workspace_.ID), id)); + return this.getEntityManager().createQuery(cq).getResultList(); + } catch (Exception ex) { + throw new DAOException(ProductDAO.ErrorKeys.ERROR_FIND_PRODUCTS_BY_WORKSPACE_ID, ex); + } + } + + @Transactional(value = Transactional.TxType.REQUIRED, rollbackOn = DAOException.class) + public void deleteProduct(String id) { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Product.class); + var root = cq.from(Product.class); + + cq.where(cb.equal(root.get(Product_.ID), id)); + var product = this.getEntityManager().createQuery(cq).getSingleResult(); + this.getEntityManager().remove(product); + } + + public enum ErrorKeys { + + ERROR_FIND_PRODUCTS_BY_WORKSPACE_ID, + + } +} diff --git a/src/main/java/io/github/onecx/workspace/domain/daos/WorkspaceDAO.java b/src/main/java/io/github/onecx/workspace/domain/daos/WorkspaceDAO.java new file mode 100644 index 0000000..fd42783 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/daos/WorkspaceDAO.java @@ -0,0 +1,113 @@ +package io.github.onecx.workspace.domain.daos; + +import static io.github.onecx.workspace.domain.models.Workspace_.THEME; +import static io.github.onecx.workspace.domain.models.Workspace_.WORKSPACE_NAME; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.NoResultException; +import jakarta.persistence.criteria.Predicate; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.daos.Page; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.utils.QueryCriteriaUtil; + +import io.github.onecx.workspace.domain.criteria.WorkspaceSearchCriteria; +import io.github.onecx.workspace.domain.models.Workspace; + +@ApplicationScoped +public class WorkspaceDAO extends AbstractDAO { + + /** + * This method fetches a workspace with + * workspaceName provided as a param and + * tenantId provided as a param + * + * @return Workspace entity if exists otherwise null + */ + public Workspace findByWorkspaceName(String workspaceName) { + + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Workspace.class); + var root = cq.from(Workspace.class); + + cq.where(cb.equal(root.get(WORKSPACE_NAME), workspaceName)); + + return this.getEntityManager().createQuery(cq).getSingleResult(); + } catch (NoResultException nre) { + return null; + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_WORKSPACE_NAME, ex); + } + } + + /** + * This method fetches the whole workspace with all his lazy load objects + * workspaceName provided as a param and + * tenantId provided as a param + * + * @return Workspace entity if exists otherwise null + */ + public Workspace loadByWorkspaceName(String workspaceName) { + + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Workspace.class); + var root = cq.from(Workspace.class); + + cq.where(cb.equal(root.get(WORKSPACE_NAME), workspaceName)); + + var workspaceQuery = this.getEntityManager().createQuery(cq); + workspaceQuery.setHint(HINT_LOAD_GRAPH, + this.getEntityManager().getEntityGraph(Workspace.WORKSPACE_FULL)); + + return this.getEntityManager().createQuery(cq).getSingleResult(); + } catch (NoResultException nre) { + return null; + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_WORKSPACE_NAME, ex); + } + } + + public PageResult findBySearchCriteria(WorkspaceSearchCriteria criteria) { + try { + var cb = getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Workspace.class); + var workspaceTable = cq.from(Workspace.class); + + List predicates = new ArrayList<>(); + if (criteria.getWorkspaceName() != null && !criteria.getWorkspaceName().isEmpty()) { + predicates.add( + cb.like(workspaceTable.get(WORKSPACE_NAME), + QueryCriteriaUtil.wildcard(criteria.getWorkspaceName(), false))); + } + if (criteria.getThemeName() != null && !criteria.getThemeName().isEmpty()) { + predicates.add( + cb.like(workspaceTable.get(THEME), QueryCriteriaUtil.wildcard(criteria.getThemeName(), false))); + } + if (!predicates.isEmpty()) { + cq.where(cb.and(predicates.toArray(new Predicate[0]))); + } + + cq.orderBy(cb.asc(workspaceTable.get(WORKSPACE_NAME))); + + return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_BY_CRITERIA, ex); + } + } + + public enum ErrorKeys { + + ERROR_FIND_BY_BASE_URL, + + ERROR_FIND_BY_CRITERIA, + ERROR_FIND_WORKSPACE_NAME, + } + +} diff --git a/src/main/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportService.java b/src/main/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportService.java new file mode 100644 index 0000000..36955f3 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportService.java @@ -0,0 +1,136 @@ +package io.github.onecx.workspace.domain.di; + +import java.util.LinkedList; +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.dataimport.DataImport; +import org.tkit.quarkus.dataimport.DataImportConfig; +import org.tkit.quarkus.dataimport.DataImportService; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gen.io.github.onecx.workspace.di.workspace.v1.model.ImportRequestDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.MenuItemStructureDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.WorkspaceDataImportDTOV1; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.domain.daos.ProductDAO; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.di.mappers.WorkspaceDataImportMapperV1; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.domain.models.Workspace; + +@DataImport("workspace") +public class WorkspaceDataImportService implements DataImportService { + + @Inject + MenuItemDAO menuItemDAO; + + @Inject + WorkspaceDAO workspaceDAO; + + @Inject + ProductDAO productDAO; + + @Inject + ObjectMapper objectMapper; + + @Inject + WorkspaceDataImportMapperV1 mapper; + + @Override + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void importData(DataImportConfig config) { + try { + var operation = config.getMetadata().getOrDefault("operation", "NONE"); + if ("NONE".equals(operation)) { + return; + } + if ("CLEAN_INSERT".equals(operation)) { + var data = objectMapper.readValue(config.getData(), WorkspaceDataImportDTOV1.class); + cleanInsert(data); + } + } catch (Exception ex) { + throw new ImportException(ex.getMessage(), ex); + } + } + + public void cleanInsert(WorkspaceDataImportDTOV1 data) { + if (data == null) { + return; + } + + // clean data + productDAO.deleteAll(); + menuItemDAO.deleteAll(); + workspaceDAO.deleteAll(); + + // import portals + importRequests(data); + } + + public void importRequests(WorkspaceDataImportDTOV1 data) { + if (data.getRequests() == null) { + return; + } + + for (var request : data.getRequests()) { + try { + importRequest(request); + } catch (Exception ex) { + if (request.getWorkspace() != null && request.getWorkspace().getWorkspaceName() != null) { + throw new ImportException("Error import portal " + request.getWorkspace().getWorkspaceName(), ex); + } else { + throw new ImportException("Error import portal", ex); + } + } + } + } + + public void importRequest(ImportRequestDTOV1 importRequestDTO) { + + var dto = importRequestDTO.getWorkspace(); + var workspace = mapper.createWorkspace(dto); + + workspace = workspaceDAO.create(workspace); + + if (importRequestDTO.getMenuItems() != null && !importRequestDTO.getMenuItems().isEmpty()) { + menuItemDAO.deleteAllMenuItemsByWorkspaceId(workspace.getId()); + List menus = new LinkedList<>(); + recursiveMappingTreeStructure(importRequestDTO.getMenuItems(), workspace, null, menus); + menuItemDAO.create(menus); + } + + } + + public void recursiveMappingTreeStructure(List items, Workspace workspace, MenuItem parent, + List mappedItems) { + int position = 0; + for (MenuItemStructureDTOV1 item : items) { + if (item != null) { + MenuItem menu = mapper.mapMenu(item); + menu.setWorkspace(workspace); + menu.setWorkspaceName(workspace.getWorkspaceName()); + menu.setPosition(position); + menu.setParent(parent); + mappedItems.add(menu); + position++; + + if (item.getChildren() == null || item.getChildren().isEmpty()) { + continue; + } + + recursiveMappingTreeStructure(item.getChildren(), workspace, menu, mappedItems); + } + } + } + + public static class ImportException extends RuntimeException { + + public ImportException(String message, Throwable ex) { + super(message, ex); + } + } +} diff --git a/src/main/java/io/github/onecx/workspace/domain/di/mappers/WorkspaceDataImportMapperV1.java b/src/main/java/io/github/onecx/workspace/domain/di/mappers/WorkspaceDataImportMapperV1.java new file mode 100644 index 0000000..3eb41cd --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/di/mappers/WorkspaceDataImportMapperV1.java @@ -0,0 +1,77 @@ +package io.github.onecx.workspace.domain.di.mappers; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.mapstruct.*; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gen.io.github.onecx.workspace.di.workspace.v1.model.MenuItemStructureDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.MicrofrontendDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.ProductDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.WorkspaceImportDTOV1; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.domain.models.Microfrontend; +import io.github.onecx.workspace.domain.models.Product; +import io.github.onecx.workspace.domain.models.Workspace; + +@Mapper(uses = OffsetDateTimeMapper.class) +public abstract class WorkspaceDataImportMapperV1 { + + @Inject + ObjectMapper mapper; + + @Mapping(target = "id", ignore = true) + @Mapping(target = "theme", source = "themeName") + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + public abstract Workspace createWorkspace(WorkspaceImportDTOV1 workspaceDTO); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "workspace", ignore = true) + public abstract Product createWorkspace(ProductDTOV1 productDTO); + + @Mapping(target = "id", ignore = true) + public abstract Microfrontend createWorkspace(MicrofrontendDTOV1 mfeDTO); + + public String map(List value) { + if (value == null || value.isEmpty()) { + return null; + } + return String.join(",", value); + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "workspaceName", ignore = true) + @Mapping(target = "applicationId", ignore = true) + @Mapping(target = "badge", ignore = true) + @Mapping(target = "children", ignore = true) + @Mapping(target = "modificationCount", ignore = true, defaultValue = "0") + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "workspace", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "permission", ignore = true) + @Mapping(target = "scope", ignore = true) + @Mapping(target = "parent", ignore = true) + public abstract MenuItem mapMenu(MenuItemStructureDTOV1 menuItemStructureDto); + +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/MenuItem.java b/src/main/java/io/github/onecx/workspace/domain/models/MenuItem.java new file mode 100644 index 0000000..02b24fd --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/MenuItem.java @@ -0,0 +1,100 @@ +package io.github.onecx.workspace.domain.models; + +import static jakarta.persistence.CascadeType.ALL; +import static jakarta.persistence.CascadeType.REFRESH; +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; + +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import io.github.onecx.workspace.domain.models.enums.Scope; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "WS_MENU_ITEM", indexes = { + @Index(columnList = "ITEM_WORKSPACE", name = "WS_MENU_ITEM_ITEM_WORKSPACE_IDX"), + @Index(columnList = "ITEM_PARENT", name = "WS_MENU_ITEM_ITEM_PARENT_IDX"), +}, uniqueConstraints = { + @UniqueConstraint(name = "WS_MENU_ITEM_ITEM_KEY_WORKSPACE", columnNames = { "ITEM_KEY", "ITEM_WORKSPACE" }) +}) +@NamedEntityGraph(name = MenuItem.MENU_ITEM_WORKSPACE_AND_TRANSLATIONS, attributeNodes = { @NamedAttributeNode("i18n"), + @NamedAttributeNode("workspace") }) +@NamedEntityGraph(name = "MenuItem.loadById", includeAllAttributes = true, attributeNodes = { @NamedAttributeNode("i18n"), + @NamedAttributeNode("children") }) +@SuppressWarnings("squid:S2160") +public class MenuItem extends TraceableEntity { + + public static final String MENU_ITEM_WORKSPACE_AND_TRANSLATIONS = "MenuItem.workspaceAndTranslations"; + + @ManyToOne(cascade = { REFRESH }, optional = false) + @JoinColumn(name = "ITEM_WORKSPACE") + Workspace workspace; + + @Column(name = "ITEM_KEY") + private String key; + + @Column(name = "ITEM_NAME") + private String name; + + @Column(name = "ITEM_DESCRIPTION") + private String description; + + @Column(name = "ITEM_URL") + private String url; + + @Column(name = "ITEM_WORKSPACE_NAME") + private String workspaceName; + + @Column(name = "APPLICATION_ID") + private String applicationId; + + @Column(name = "ITEM_DISABLED") + @NotNull + private boolean disabled; + + @Column(name = "ITEM_POS") + private int position; + + @Column(name = "ITEM_PERMISSION_OBJECT") + private String permission; + + @Column(name = "ITEM_BADGE") + private String badge; + + @Column(name = "ITEM_SCOPE") + @Enumerated(STRING) + private Scope scope; + + @Column(name = "WORKSPACE_EXIT") + @NotNull + private boolean workspaceExit; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "ITEM_PARENT") + private MenuItem parent; + + @OneToMany(fetch = LAZY, cascade = ALL, mappedBy = "parent", orphanRemoval = true) + @OrderBy("position") + private Set children = new HashSet<>(); + + @ElementCollection + @MapKeyColumn(name = "LANGUAGE") + @Column(name = "i18n") + @CollectionTable(name = "WS_MENU_ITEM_I18N") + private Map i18n = new HashMap<>(); + + @Column(name = "ROLES", columnDefinition = "TEXT") + private String roles; + +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/Microfrontend.java b/src/main/java/io/github/onecx/workspace/domain/models/Microfrontend.java new file mode 100644 index 0000000..4d0ba06 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/Microfrontend.java @@ -0,0 +1,30 @@ +package io.github.onecx.workspace.domain.models; + +import java.io.Serializable; +import java.util.UUID; + +import jakarta.persistence.*; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "MICROFRONTEND", uniqueConstraints = { + @UniqueConstraint(name = "MFE_ID_PATH_PRODUCT_GUID", columnNames = { "MFE_ID", "BASE_PATH", "PRODUCT_GUID" }) +}) +@SuppressWarnings("squid:S2160") +public class Microfrontend implements Serializable { + + @Id + @Column(name = "GUID") + private String id = UUID.randomUUID().toString(); + + @Column(name = "MFE_ID", nullable = false) + private String mfeId; + + @Column(name = "BASE_PATH", nullable = false) + private String basePath; + +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/Product.java b/src/main/java/io/github/onecx/workspace/domain/models/Product.java new file mode 100644 index 0000000..095ea08 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/Product.java @@ -0,0 +1,38 @@ +package io.github.onecx.workspace.domain.models; + +import static jakarta.persistence.FetchType.EAGER; +import static jakarta.persistence.FetchType.LAZY; + +import java.util.List; + +import jakarta.persistence.*; + +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "PRODUCT", uniqueConstraints = { + @UniqueConstraint(name = "PRODUCT_NAME_WORKSPACE_GUID", columnNames = { "PRODUCT_NAME", "WORKSPACE_GUID" }) +}) +@SuppressWarnings("squid:S2160") +public class Product extends TraceableEntity { + + @Column(name = "PRODUCT_NAME") + private String productName; + + @Column(name = "BASE_URL", unique = true) + private String baseUrl; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "WORKSPACE_GUID") + private Workspace workspace; + + @OneToMany(fetch = EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "PRODUCT_GUID") + private List microfrontends; + +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/SubjectLink.java b/src/main/java/io/github/onecx/workspace/domain/models/SubjectLink.java new file mode 100644 index 0000000..86eac88 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/SubjectLink.java @@ -0,0 +1,15 @@ +package io.github.onecx.workspace.domain.models; + +import jakarta.persistence.Embeddable; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Embeddable +public class SubjectLink { + + private String label; + private String url; +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/Workspace.java b/src/main/java/io/github/onecx/workspace/domain/models/Workspace.java new file mode 100644 index 0000000..770073a --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/Workspace.java @@ -0,0 +1,83 @@ +package io.github.onecx.workspace.domain.models; + +import static jakarta.persistence.FetchType.LAZY; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.persistence.*; + +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "WORKSPACE", uniqueConstraints = { + @UniqueConstraint(name = "WORKSPACE_NAME_TENANT_ID", columnNames = { "WORKSPACE_NAME", "TENANT_ID" }), + @UniqueConstraint(name = "WORKSPACE_BASE_URL", columnNames = { "BASE_URL" }) +}) +@NamedEntityGraph(name = Workspace.WORKSPACE_FULL, attributeNodes = { @NamedAttributeNode("subjectLinks"), + @NamedAttributeNode("imageUrls"), @NamedAttributeNode(value = "products") }) +@SuppressWarnings("squid:S2160") +public class Workspace extends TraceableEntity { + + public static final String WORKSPACE_FULL = "Workspace.full"; + + @Column(name = "TENANT_ID") + private String tenantId; + + @Column(name = "WORKSPACE_NAME", nullable = false) + private String workspaceName; + + @Column(name = "DESCRIPTION") + private String description; + + @Column(name = "THEME") + private String theme; + + @Column(name = "HOME_PAGE") + private String homePage; + + @Column(name = "BASE_URL", unique = true) + private String baseUrl; + + @Column(name = "COMPANY_NAME") + private String companyName; + + @Embedded + private WorkspaceAddress address; + + @Column(name = "PHONE_NUMBER") + private String phoneNumber; + + @Column(name = "RSS_FEED_URL") + private String rssFeedUrl; + + @Column(name = "FOOTER_LABEL") + private String footerLabel; + + @ElementCollection(fetch = LAZY) + @CollectionTable(name = "WS_ITEM_SUBJECT_LINKS") + @AttributeOverride(name = "label", column = @Column(name = "link_label")) + @AttributeOverride(name = "url", column = @Column(name = "link_url")) + private Set subjectLinks = new HashSet<>(); + + @ElementCollection(fetch = LAZY) + @CollectionTable(name = "WS_ITEM_IMAGE_URLS") + @Column(name = "IMAGE_URL") + private Set imageUrls = new HashSet<>(); + + @Column(name = "WORKSPACE_ROLES", columnDefinition = "TEXT") + private String workspaceRoles; + + @Column(name = "LOGO_URL") + private String logoUrl; + + @OneToMany(mappedBy = "workspace") + private List products; + +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/WorkspaceAddress.java b/src/main/java/io/github/onecx/workspace/domain/models/WorkspaceAddress.java new file mode 100644 index 0000000..8b7d81e --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/WorkspaceAddress.java @@ -0,0 +1,28 @@ +package io.github.onecx.workspace.domain.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Embeddable +public class WorkspaceAddress { + + @Column(name = "street") + private String street; + + @Column(name = "streetNo") + private String streetNo; + + @Column(name = "city") + private String city; + + @Column(name = "country") + private String country; + + @Column(name = "postalCode") + private String postalCode; +} diff --git a/src/main/java/io/github/onecx/workspace/domain/models/enums/Scope.java b/src/main/java/io/github/onecx/workspace/domain/models/enums/Scope.java new file mode 100644 index 0000000..11a4f4e --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/domain/models/enums/Scope.java @@ -0,0 +1,8 @@ +package io.github.onecx.workspace.domain.models.enums; + +public enum Scope { + + WORKSPACE, + APP, + PAGE +} \ No newline at end of file diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestController.java b/src/main/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestController.java new file mode 100644 index 0000000..a536763 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestController.java @@ -0,0 +1,267 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.BAD_REQUEST; +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.NOT_FOUND; + +import java.util.*; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.workspace.rs.internal.MenuInternalApi; +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.rs.internal.mappers.InternalExceptionMapper; +import io.github.onecx.workspace.rs.internal.mappers.MenuItemMapper; + +@LogService +@ApplicationScoped +@Transactional(Transactional.TxType.NOT_SUPPORTED) +public class MenuInternalRestController implements MenuInternalApi { + + @Inject + InternalExceptionMapper exceptionMapper; + + @Context + UriInfo uriInfo; + + @Inject + MenuItemMapper mapper; + + @Inject + MenuItemDAO dao; + + @Inject + WorkspaceDAO workspaceDAO; + + @Override + public Response createMenuItemForWorkspace(String id, CreateMenuItemDTO menuItemDTO) { + var workspace = workspaceDAO.findById(id); + + if (workspace == null) { + throw new ConstraintException("Workspace does not exist", MenuItemErrorKeys.WORKSPACE_DOES_NOT_EXIST, null); + } + + MenuItem parentItem = null; + if (menuItemDTO.getParentItemId() != null) { + + parentItem = dao.findById(menuItemDTO.getParentItemId()); + + if (parentItem == null) { + throw new ConstraintException("Parent menu item does not exist", MenuItemErrorKeys.PARENT_MENU_DOES_NOT_EXIST, + null); + } else { + // check if parent's portal and child's portal are the same + if (!parentItem.getWorkspace().getId().equals(id)) { + throw new ConstraintException("Parent menu item and menu item does not have the same workspace", + MenuItemErrorKeys.WORKSPACE_DIFFERENT, null); + } + } + } + + var menuItem = mapper.create(menuItemDTO); + menuItem.setWorkspace(workspace); + menuItem.setWorkspaceName(workspace.getWorkspaceName()); + menuItem.setParent(parentItem); + menuItem = dao.create(menuItem); + + return Response + .created(uriInfo.getAbsolutePathBuilder().path(menuItem.getId()).build()) + .build(); + } + + @Override + public Response deleteAllMenuItemsForWorkspace(String id) { + dao.deleteAllMenuItemsByWorkspaceId(id); + + return Response.noContent().build(); + } + + @Override + public Response deleteMenuItemById(String id, String menuItemId) { + dao.deleteQueryById(menuItemId); + + return Response.noContent().build(); + } + + @Override + public Response getMenuItemById(String id, String menuItemId) { + var result = dao.findById(menuItemId); + + return Response.ok(mapper.map(result)).build(); + } + + @Override + public Response getMenuItemsForWorkspaceId(String id) { + var result = dao.loadAllMenuItemsByWorkspaceId(id); + + return Response.ok(mapper.mapList(result)).build(); + } + + @Override + public Response getMenuStructureForWorkspaceId(String id) { + var result = dao.loadAllMenuItemsByWorkspaceId(id); + + return Response.ok(mapper.mapTree(result)).build(); + } + + @Override + public Response patchMenuItems(String id, List menuItemDTO) { + // create map of + Map tmp = menuItemDTO.stream() + .collect(Collectors.toMap(MenuItemDTO::getId, x -> x)); + + // load menu items + var items = dao.findByIds(Arrays.asList(tmp.keySet().toArray())).toList(); + if (items.isEmpty()) { + return Response.status(NOT_FOUND).build(); + } + + if (items.size() != tmp.size()) { + return Response.status(NOT_FOUND).entity("Menu Items specified in request body do not exist in db").build(); + } + + for (MenuItem item : items) { + MenuItemDTO dto = tmp.get(item.getId()); + + // update parent + var response = updateParent(item, dto); + if (response != null) { + return response; + } + + mapper.update(dto, item); + } + + var result = dao.update(items); + return Response.ok(mapper.map(result)).build(); + } + + @Override + public Response updateMenuItem(String id, String menuItemId, MenuItemDTO menuItemDTO) { + var menuItem = dao.findById(menuItemId); + if (menuItem == null) { + return Response.status(NOT_FOUND).build(); + } + + // update parent + var response = updateParent(menuItem, menuItemDTO); + if (response != null) { + return response; + } + + mapper.update(menuItemDTO, menuItem); + + return Response.ok(mapper.map(menuItem)).build(); + } + + @Override + public Response uploadMenuStructureForWorkspaceId(String id, WorkspaceMenuItemStructrueDTO menuItemStructrueDTO) { + var workspace = workspaceDAO.findById(id); + if (workspace == null) { + throw new ConstraintException("Given workspace does not exist", MenuItemErrorKeys.WORKSPACE_DOES_NOT_EXIST, null); + } + + if (menuItemStructrueDTO.getMenuItems() == null || menuItemStructrueDTO.getMenuItems().isEmpty()) { + throw new ConstraintException("menuItems cannot be null", MenuItemErrorKeys.MENU_ITEMS_NULL, null); + } + + List items = new LinkedList<>(); + mapper.recursiveMappingTreeStructure(menuItemStructrueDTO.getMenuItems(), workspace, null, items); + + dao.deleteAllMenuItemsByWorkspaceId(id); + dao.create(items); + + return Response.noContent().build(); + + } + + private Response updateParent(MenuItem menuItem, MenuItemDTO dto) { + + if (dto.getParentItemId() == null) { + menuItem.setParent(null); + return null; + } + + // check parent change + if (menuItem.getParent() != null && dto.getParentItemId().equals(menuItem.getParent().getId())) { + return null; + } + + // checking if request parent id is the same as current id + if (dto.getParentItemId().equals(menuItem.getId())) { + // TODO: make a failed contraint error + return Response.status(BAD_REQUEST).entity("Menu Item " + menuItem.getId() + " id and parentItem id are the same") + .build(); + } + + // checking if parent exists + var parent = dao.findById(dto.getParentItemId()); + if (parent == null) { + // TODO: make a failed contraint error + return Response.status(BAD_REQUEST) + .entity("Parent menu item " + dto.getParentItemId() + " does not exists").build(); + } else { + + // checking if parent exists in the same portal + if (!parent.getWorkspace().getId().equals(menuItem.getWorkspace().getId())) { + // TODO: make a failed contraint error + return Response.status(BAD_REQUEST).entity("Parent menu item is assigned to different portal").build(); + } + + // check for cycle + Set children = new HashSet<>(); + children(menuItem, children); + if (children.contains(parent.getId())) { + // TODO: make a failed contraint error + return Response.status(BAD_REQUEST).entity( + "One of the items try to set one of its children to the new parent. Cycle dependency can not be created in tree structure") + .build(); + } + } + + // set new parent + menuItem.setParent(parent); + + return null; + } + + private void children(MenuItem menuItem, Set result) { + menuItem.getChildren().forEach(c -> { + result.add(c.getId()); + children(c, result); + }); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + enum MenuItemErrorKeys { + WORKSPACE_DOES_NOT_EXIST, + PARENT_MENU_DOES_NOT_EXIST, + WORKSPACE_DIFFERENT, + + MENU_ITEMS_NULL, + + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/controllers/ProductInternalRestController.java b/src/main/java/io/github/onecx/workspace/rs/internal/controllers/ProductInternalRestController.java new file mode 100644 index 0000000..73c6723 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/controllers/ProductInternalRestController.java @@ -0,0 +1,103 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.NOT_FOUND; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.workspace.rs.internal.ProductInternalApi; +import gen.io.github.onecx.workspace.rs.internal.model.CreateProductRequestDTO; +import gen.io.github.onecx.workspace.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.workspace.rs.internal.model.UpdateProductRequestDTO; +import io.github.onecx.workspace.domain.daos.ProductDAO; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.rs.internal.mappers.InternalExceptionMapper; +import io.github.onecx.workspace.rs.internal.mappers.ProductMapper; + +@LogService +@ApplicationScoped +@Transactional(Transactional.TxType.NOT_SUPPORTED) +public class ProductInternalRestController implements ProductInternalApi { + + @Inject + InternalExceptionMapper exceptionMapper; + + @Context + UriInfo uriInfo; + + @Inject + ProductDAO dao; + + @Inject + ProductMapper mapper; + + @Inject + WorkspaceDAO workspaceDAO; + + @Override + public Response createProductInWorkspace(String id, CreateProductRequestDTO createProductRequestDTO) { + var workspace = workspaceDAO.findById(id); + if (workspace == null) { + throw new ConstraintException("Workspace does not exist", + ProductInternalRestController.ProductErrorKeys.WORKSPACE_DOES_NOT_EXIST, null); + } + var product = mapper.create(createProductRequestDTO); + product.setWorkspace(workspace); + product = dao.create(product); + + return Response + .created(uriInfo.getAbsolutePathBuilder().path(product.getId()).build()) + .entity(mapper.map(product)) + .build(); + } + + @Override + public Response deleteProductById(String id, String productId) { + dao.deleteProduct(productId); + + return Response.noContent().build(); + } + + @Override + public Response getProductsForWorkspaceId(String id) { + var result = dao.getProductsForWorkspaceId(id); + return Response.ok(mapper.map(result)).build(); + } + + @Override + public Response updateProductById(String id, String productId, UpdateProductRequestDTO updateProductRequestDTO) { + var product = dao.findById(productId); + if (product == null) { + return Response.status(NOT_FOUND).build(); + } + + mapper.update(updateProductRequestDTO, product); + + return Response.ok(mapper.map(product)).build(); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + enum ProductErrorKeys { + WORKSPACE_DOES_NOT_EXIST, + + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestController.java b/src/main/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestController.java new file mode 100644 index 0000000..40f1bd6 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestController.java @@ -0,0 +1,112 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.workspace.rs.internal.WorkspaceInternalApi; +import gen.io.github.onecx.workspace.rs.internal.model.CreateWorkspaceRequestDTO; +import gen.io.github.onecx.workspace.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.workspace.rs.internal.model.UpdateWorkspaceRequestDTO; +import gen.io.github.onecx.workspace.rs.internal.model.WorkspaceSearchCriteriaDTO; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.models.Workspace; +import io.github.onecx.workspace.rs.internal.mappers.InternalExceptionMapper; +import io.github.onecx.workspace.rs.internal.mappers.WorkspaceMapper; + +@LogService +@ApplicationScoped +@Transactional(Transactional.TxType.NOT_SUPPORTED) +public class WorkspaceInternalRestController implements WorkspaceInternalApi { + + @Inject + InternalExceptionMapper exceptionMapper; + + @Inject + WorkspaceMapper workspaceMapper; + + @Inject + WorkspaceDAO dao; + + @Inject + MenuItemDAO menuDao; + + @Context + UriInfo uriInfo; + + @Override + public Response createWorkspace(CreateWorkspaceRequestDTO createWorkspaceRequestDTO) { + var workspace = workspaceMapper.create(createWorkspaceRequestDTO); + workspace = dao.create(workspace); + return Response + .created(uriInfo.getAbsolutePathBuilder().path(workspace.getId()).build()) + .entity(workspaceMapper.map(workspace)) + .build(); + } + + @Override + public Response deleteWorkspace(String id) { + // delete menu before deleting workspace + menuDao.deleteAllMenuItemsByWorkspaceId(id); + + dao.deleteQueryById(id); + return Response.noContent().build(); + } + + @Override + public Response getWorkspace(String id) { + var item = dao.findById(id); + if (item == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(workspaceMapper.map(item)).build(); + } + + @Override + public Response searchWorkspace(WorkspaceSearchCriteriaDTO workspaceSearchCriteriaDTO) { + var criteria = workspaceMapper.map(workspaceSearchCriteriaDTO); + var result = dao.findBySearchCriteria(criteria); + return Response.ok(workspaceMapper.mapPageResult(result)).build(); + } + + @Override + public Response updateWorkspace(String id, UpdateWorkspaceRequestDTO updateWorkspaceRequestDTO) { + Workspace workspace = dao.findById(id); + if (workspace == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + // update portalItemName for all portal's menu items + var newWorkspaceName = updateWorkspaceRequestDTO.getWorkspaceName(); + var oldWorkspaceName = workspace.getWorkspaceName(); + + if (!oldWorkspaceName.equals(newWorkspaceName)) { + menuDao.updateMenuItems(newWorkspaceName, oldWorkspaceName, updateWorkspaceRequestDTO.getBaseUrl()); + } + + workspaceMapper.update(updateWorkspaceRequestDTO, workspace); + dao.update(workspace); + + return Response.noContent().build(); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/log/MenuItemLogParam.java b/src/main/java/io/github/onecx/workspace/rs/internal/log/MenuItemLogParam.java new file mode 100644 index 0000000..5df9264 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/log/MenuItemLogParam.java @@ -0,0 +1,35 @@ +package io.github.onecx.workspace.rs.internal.log; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.tkit.quarkus.log.cdi.LogParam; + +import gen.io.github.onecx.workspace.rs.internal.model.CreateMenuItemDTO; +import gen.io.github.onecx.workspace.rs.internal.model.MenuItemDTO; +import gen.io.github.onecx.workspace.rs.internal.model.WorkspaceMenuItemStructrueDTO; + +@ApplicationScoped +public class MenuItemLogParam implements LogParam { + + @Override + public List getClasses() { + return List.of( + this.item(10, MenuItemDTO.class, + x -> "MenuItem[ key: " + ((MenuItemDTO) x).getKey() + + ", workspaceName: " + ((MenuItemDTO) x).getWorkspaceName() + " ]"), + this.item(10, CreateMenuItemDTO.class, + x -> "CreateMenuItemDTO[ key: " + ((CreateMenuItemDTO) x).getKey() + + ", url: " + ((CreateMenuItemDTO) x).getUrl() + " ]"), + this.item(10, WorkspaceMenuItemStructrueDTO.class, + x -> "WorkspaceMenuItemStructrueDTO[ menu items size: " + + (((WorkspaceMenuItemStructrueDTO) x).getMenuItems() != null + ? ((WorkspaceMenuItemStructrueDTO) x).getMenuItems().size() + : "null") + + " ]") + + ); + + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/log/ProductLogParam.java b/src/main/java/io/github/onecx/workspace/rs/internal/log/ProductLogParam.java new file mode 100644 index 0000000..443fd0c --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/log/ProductLogParam.java @@ -0,0 +1,34 @@ +package io.github.onecx.workspace.rs.internal.log; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.tkit.quarkus.log.cdi.LogParam; + +import gen.io.github.onecx.workspace.rs.internal.model.CreateProductRequestDTO; +import gen.io.github.onecx.workspace.rs.internal.model.UpdateProductRequestDTO; + +@ApplicationScoped +public class ProductLogParam implements LogParam { + + @Override + public List getClasses() { + return List.of( + this.item(10, CreateProductRequestDTO.class, + x -> "CreateProductRequestDTO[ name: " + ((CreateProductRequestDTO) x).getProductName() + + ", baseUrl: " + ((CreateProductRequestDTO) x).getBaseUrl() + + ", mfe list size: " + + (((CreateProductRequestDTO) x).getMicrofrontends() != null + ? ((CreateProductRequestDTO) x).getMicrofrontends().size() + : "null") + + " ]"), + this.item(10, UpdateProductRequestDTO.class, + x -> "UpdateProductRequestDTO[ baseUrl: " + ((UpdateProductRequestDTO) x).getBaseUrl() + + ", mfe list size: " + + (((UpdateProductRequestDTO) x).getMicrofrontends() != null + ? ((UpdateProductRequestDTO) x).getMicrofrontends().size() + : "null") + + " ]")); + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/log/WorkspaceLogParam.java b/src/main/java/io/github/onecx/workspace/rs/internal/log/WorkspaceLogParam.java new file mode 100644 index 0000000..fbbda91 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/log/WorkspaceLogParam.java @@ -0,0 +1,30 @@ +package io.github.onecx.workspace.rs.internal.log; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.tkit.quarkus.log.cdi.LogParam; + +import gen.io.github.onecx.workspace.rs.internal.model.*; + +@ApplicationScoped +public class WorkspaceLogParam implements LogParam { + + @Override + public List getClasses() { + return List.of( + this.item(10, CreateWorkspaceRequestDTO.class, + x -> "CreateWorkspaceRequestDTO[ name: " + ((CreateWorkspaceRequestDTO) x).getWorkspaceName() + + ", baseUrl: " + ((CreateWorkspaceRequestDTO) x).getBaseUrl() + + ", company name: " + ((CreateWorkspaceRequestDTO) x).getCompanyName() + + " ]"), + this.item(10, UpdateWorkspaceRequestDTO.class, + x -> "UpdateWorkspaceRequestDTO[ name: " + ((UpdateWorkspaceRequestDTO) x).getWorkspaceName() + + ", baseUrl: " + ((UpdateWorkspaceRequestDTO) x).getBaseUrl() + + ", company name: " + ((UpdateWorkspaceRequestDTO) x).getCompanyName() + + " ]") + + ); + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/mappers/InternalExceptionMapper.java b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/InternalExceptionMapper.java new file mode 100644 index 0000000..56a780f --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/InternalExceptionMapper.java @@ -0,0 +1,72 @@ +package io.github.onecx.workspace.rs.internal.mappers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.workspace.rs.internal.model.ProblemDetailInvalidParamDTO; +import gen.io.github.onecx.workspace.rs.internal.model.ProblemDetailParamDTO; +import gen.io.github.onecx.workspace.rs.internal.model.ProblemDetailResponseDTO; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Mapper(uses = { OffsetDateTimeMapper.class }) +public abstract class InternalExceptionMapper { + + @LogService(log = false) + public RestResponse constraint(ConstraintViolationException ex) { + var dto = exception("CONSTRAINT_VIOLATIONS", ex.getMessage()); + dto.setInvalidParams(createErrorValidationResponse(ex.getConstraintViolations())); + return RestResponse.status(Response.Status.BAD_REQUEST, dto); + } + + @LogService(log = false) + public RestResponse exception(ConstraintException ce) { + var e = exception(ce.getMessageKey().name(), ce.getConstraints()); + e.setParams(map(ce.namedParameters)); + return RestResponse.status(Response.Status.BAD_REQUEST, e); + } + + public List map(Map params) { + if (params == null) { + return List.of(); + } + return params.entrySet().stream().map(e -> { + var item = new ProblemDetailParamDTO(); + item.setKey(e.getKey()); + if (e.getValue() != null) { + item.setValue(e.getValue().toString()); + } + return item; + }).toList(); + } + + @Mapping(target = "invalidParams", ignore = true) + @Mapping(target = "removeInvalidParamsItem", ignore = true) + @Mapping(target = "removeParamsItem", ignore = true) + @Mapping(target = "params", ignore = true) + public abstract ProblemDetailResponseDTO exception(String errorCode, String detail); + + public abstract List createErrorValidationResponse( + Set> constraintViolation); + + @Mapping(target = "name", source = "propertyPath") + @Mapping(target = "message", source = "message") + public abstract ProblemDetailInvalidParamDTO createError(ConstraintViolation constraintViolation); + + public String mapPath(Path path) { + return path.toString(); + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/mappers/MenuItemMapper.java b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/MenuItemMapper.java new file mode 100644 index 0000000..86b74fb --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/MenuItemMapper.java @@ -0,0 +1,138 @@ +package io.github.onecx.workspace.rs.internal.mappers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.mapstruct.*; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.domain.models.Workspace; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public abstract class MenuItemMapper { + + public abstract List map(Stream items); + + @Mapping(target = "workspaceName", ignore = true) + @Mapping(target = "workspace", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "parent", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "children", ignore = true) + @Mapping(target = "applicationId", ignore = true) + public abstract MenuItem create(CreateMenuItemDTO dto); + + @Mapping(target = "workspace", ignore = true) + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + @Mapping(target = "id", ignore = true) + @Mapping(target = "modificationCount", ignore = true, defaultValue = "0") + @Mapping(target = "workspaceName", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "permission", ignore = true) + @Mapping(target = "parent", ignore = true) + @Mapping(target = "children", ignore = true) + public abstract void update(MenuItemDTO menuItemDetailsDto, @MappingTarget MenuItem entity); + + public abstract List mapList(List items); + + @Mapping(target = "version", source = "modificationCount") + @Mapping(target = "removeRolesItem", ignore = true) + @Mapping(target = "removeI18nItem", ignore = true) + @Mapping(target = "parentItemId", source = "parent.id") + public abstract MenuItemDTO map(MenuItem item); + + public List mapStringToList(String value) { + if (value == null || value.isEmpty()) { + return new ArrayList<>(); + } + return Arrays.asList(value.split(",")); + } + + public String mapListToString(List list) { + if (list == null || list.isEmpty()) { + return null; + } + return String.join(",", list); + } + + public WorkspaceMenuItemStructrueDTO mapTree(Collection entities) { + WorkspaceMenuItemStructrueDTO dto = new WorkspaceMenuItemStructrueDTO(); + if (entities.isEmpty()) { + dto.setMenuItems(new ArrayList<>()); + return dto; + } + + var parentChildrenMap = entities.stream() + .collect(Collectors + .groupingBy(menuItem -> menuItem.getParent() == null ? "TOP" : menuItem.getParent().getKey())); + + dto.setMenuItems(parentChildrenMap.get("TOP").stream().map(this::mapTreeItem).toList()); + return dto; + } + + public abstract List mapCollection(Collection entities); + + @Mapping(target = "parentItemId", source = "parent.id") + @Mapping(target = "version", source = "modificationCount") + @Mapping(target = "removeI18nItem", ignore = true) + @Mapping(target = "removeChildrenItem", ignore = true) + @Mapping(target = "removeRolesItem", ignore = true) + public abstract WorkspaceMenuItemDTO mapTreeItem(MenuItem entity); + + public void recursiveMappingTreeStructure(List items, Workspace workspace, MenuItem parent, + List mappedItems) { + + int position = 0; + for (WorkspaceMenuItemDTO item : items) { + if (item != null) { + MenuItem menu = mapMenu(item); + updateMenu(menu, position, workspace, parent); + mappedItems.add(menu); + position++; + + if (item.getChildren() == null || item.getChildren().isEmpty()) { + continue; + } + + recursiveMappingTreeStructure(item.getChildren(), workspace, menu, mappedItems); + } + } + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "workspaceName", ignore = true) + @Mapping(target = "children", ignore = true) + @Mapping(target = "modificationCount", ignore = true, defaultValue = "0") + @Mapping(target = "parent.id", source = "parentItemId") + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "workspace", ignore = true) + @Mapping(target = "permission", ignore = true) + @Mapping(target = "scope", ignore = true) + public abstract MenuItem mapMenu(WorkspaceMenuItemDTO menuItemStructureDto); + + public void updateMenu(MenuItem menuItem, int position, Workspace workspace, + MenuItem parent) { + menuItem.setWorkspace(workspace); + menuItem.setWorkspaceName(workspace.getWorkspaceName()); + menuItem.setPosition(position); + menuItem.setParent(parent); + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/mappers/ProductMapper.java b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/ProductMapper.java new file mode 100644 index 0000000..a3b7836 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/ProductMapper.java @@ -0,0 +1,51 @@ +package io.github.onecx.workspace.rs.internal.mappers; + +import java.util.List; + +import org.mapstruct.*; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.domain.models.Microfrontend; +import io.github.onecx.workspace.domain.models.Product; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface ProductMapper { + + @Mapping(target = "workspace", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + Product create(CreateProductRequestDTO dto); + + @Mapping(target = "id", ignore = true) + Microfrontend create(CreateMicrofrontendDTO dto); + + @Mapping(target = "workspace", ignore = true) + @Mapping(target = "productName", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + void update(UpdateProductRequestDTO dto, @MappingTarget Product product); + + @Mapping(target = "id", ignore = true) + Microfrontend update(UpdateMicrofrontendDTO dto); + + List map(List entity); + + @Mapping(target = "version", source = "modificationCount") + @Mapping(target = "removeMicrofrontendsItem", ignore = true) + ProductDTO map(Product entity); + + MicrofrontendDTO map(Microfrontend entity); +} diff --git a/src/main/java/io/github/onecx/workspace/rs/internal/mappers/WorkspaceMapper.java b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/WorkspaceMapper.java new file mode 100644 index 0000000..12cd8ce --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/internal/mappers/WorkspaceMapper.java @@ -0,0 +1,63 @@ +package io.github.onecx.workspace.rs.internal.mappers; + +import java.util.List; +import java.util.stream.Stream; + +import org.mapstruct.*; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.domain.criteria.WorkspaceSearchCriteria; +import io.github.onecx.workspace.domain.models.Workspace; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface WorkspaceMapper { + + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "products", ignore = true) + @Mapping(target = "tenantId", ignore = true) + Workspace create(CreateWorkspaceRequestDTO dto); + + @Mapping(target = "tenantId", ignore = true) + @Mapping(target = "products", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "id", ignore = true) + void update(UpdateWorkspaceRequestDTO dto, @MappingTarget Workspace workspace); + + WorkspaceSearchCriteria map(WorkspaceSearchCriteriaDTO dto); + + @Mapping(target = "removeStreamItem", ignore = true) + @Mapping(target = "stream", qualifiedByName = "mapStream") + WorkspacePageResultDTO mapPageResult(PageResult page); + + @Mapping(target = "removeSubjectLinksItem", ignore = true) + @Mapping(target = "removeImageUrlsItem", ignore = true) + @Mapping(target = "version", source = "modificationCount") + WorkspaceDTO map(Workspace data); + + @Mapping(target = "removeSubjectLinksItem", ignore = true) + @Mapping(target = "removeImageUrlsItem", ignore = true) + @Mapping(target = "imageUrls", ignore = true) + @Mapping(target = "subjectLinks", ignore = true) + @Mapping(target = "version", source = "modificationCount") + @Named("mapWithoutLazy") + WorkspaceDTO mapWithoutLazy(Workspace data); + + @Named("mapStream") + @IterableMapping(qualifiedByName = "mapWithoutLazy") + List mapStream(Stream stream); +} diff --git a/src/main/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestController.java b/src/main/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestController.java new file mode 100644 index 0000000..ada4966 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestController.java @@ -0,0 +1,54 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.workspace.rs.legacy.PortalLegacyApi; +import gen.io.github.onecx.workspace.rs.legacy.model.RestExceptionDTO; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.rs.legacy.mappers.PortalLegacyMapper; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@LogService +@ApplicationScoped +@Transactional(Transactional.TxType.NOT_SUPPORTED) +public class PortalLegacyRestController implements PortalLegacyApi { + + @Inject + MenuItemDAO dao; + + @Inject + PortalLegacyMapper mapper; + + @Override + public Response getMenuStructureForPortalIdAndApplicationId(String applicationId, String portalId) { + return getMenuStructureForPortalName(portalId); + } + + @Override + public Response getMenuStructureForPortalName(String portalName) { + var items = dao.loadAllMenuItemsByWorkspaceName(portalName); + return Response.ok(mapper.mapToTree(items)).build(); + } + + @ServerExceptionMapper + public RestResponse exception(Exception ex) { + log.error("Processing portal legacy rest controller error: {}", ex.getMessage()); + + if (ex instanceof DAOException de) { + return RestResponse.status(Response.Status.BAD_REQUEST, + mapper.exception(de.getMessageKey().name(), ex.getMessage(), de.parameters)); + } + return RestResponse.status(Response.Status.INTERNAL_SERVER_ERROR, + mapper.exception("UNDEFINED_ERROR_CODE", ex.getMessage())); + + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestController.java b/src/main/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestController.java new file mode 100644 index 0000000..7331564 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestController.java @@ -0,0 +1,119 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import java.util.LinkedList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.Response; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.workspace.rs.legacy.TkitPortalApi; +import gen.io.github.onecx.workspace.rs.legacy.model.MenuRegistrationRequestDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.MenuRegistrationResponseDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.RestExceptionDTO; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.rs.legacy.mappers.TkitPortalMapper; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@LogService +@ApplicationScoped +@Transactional(Transactional.TxType.NOT_SUPPORTED) +public class TkitPortalRestController implements TkitPortalApi { + + @ConfigProperty(name = "tkit.legacy.enable.menu.auto.registration", defaultValue = "false") + boolean enableAutoRegistration; + + @Inject + MenuItemDAO menuItemDAO; + + @Inject + WorkspaceDAO workspaceDAO; + + @Inject + TkitPortalMapper mapper; + + @Override + public Response getMenuStructureForTkitPortalName(String portalName, Boolean interpolate) { + var menuItems = menuItemDAO.loadAllMenuItemsByWorkspaceName(portalName); + + if (interpolate != null && interpolate) { + for (MenuItem item : menuItems) { + if (StringUtils.isNotBlank(item.getUrl())) { + item.setUrl(StringSubstitutor.replace(item.getUrl(), System.getenv())); + } + } + } + + return Response.ok(mapper.mapToTree(menuItems)).build(); + } + + @Override + public Response submitMenuRegistrationRequest(String portalName, String appId, + MenuRegistrationRequestDTO menuRegistrationRequestDTO) { + MenuRegistrationResponseDTO response = new MenuRegistrationResponseDTO(); + response.setApplicationId(appId); + response.setRequestVersion(menuRegistrationRequestDTO.getRequestVersion()); + if (!enableAutoRegistration) { + log.info("Auto registration of menu requests is disabled, ignoring request from {}", appId); + response.setApplied(false); + response.setNotice("TKITPORTAL10003 Menu registration request has been ignored"); + return Response.ok(response).build(); + } + + try { + var workspace = workspaceDAO.findByWorkspaceName(portalName); + if (workspace == null) { + throw new Exception("Workspace not found"); + } + + if (menuRegistrationRequestDTO.getMenuItems() == null || menuRegistrationRequestDTO.getMenuItems().isEmpty()) { + throw new Exception("Menu items are empty"); + } + + // In the old structure just a sub part of the menu was send so we need to find the parent menu item if defined + var parentKey = menuRegistrationRequestDTO.getMenuItems().get(0).getParentKey(); + MenuItem parent = null; + if (parentKey != null) { + parent = menuItemDAO.loadMenuItemByWorkspaceAndKey(portalName, parentKey); + } + + List items = new LinkedList<>(); + mapper.recursiveMappingTreeStructure(menuRegistrationRequestDTO.getMenuItems(), workspace, parent, appId, items); + + menuItemDAO.deleteAllMenuItemsByWorkspaceNameAndAppId(portalName, appId); + menuItemDAO.create(items); + + response.setApplied(true); + } catch (Exception ex) { + log.error("Failed to process menu registration request", ex); + response.setApplied(false); + response.setNotice("TKPORTAL100002 Menu registration request failed due to server error"); + } + return Response.ok(response).build(); + } + + @ServerExceptionMapper + public RestResponse exception(Exception ex) { + log.error("Processing portal legacy rest controller error: {}", ex.getMessage()); + + if (ex instanceof DAOException de) { + return RestResponse.status(Response.Status.BAD_REQUEST, + mapper.exception(de.getMessageKey().name(), ex.getMessage(), de.parameters)); + } + return RestResponse.status(Response.Status.INTERNAL_SERVER_ERROR, + mapper.exception("UNDEFINED_ERROR_CODE", ex.getMessage())); + + } +} diff --git a/src/main/java/io/github/onecx/workspace/rs/legacy/log/TkitPortalLogParam.java b/src/main/java/io/github/onecx/workspace/rs/legacy/log/TkitPortalLogParam.java new file mode 100644 index 0000000..fad6753 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/legacy/log/TkitPortalLogParam.java @@ -0,0 +1,27 @@ +package io.github.onecx.workspace.rs.legacy.log; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.tkit.quarkus.log.cdi.LogParam; + +import gen.io.github.onecx.workspace.rs.legacy.model.MenuRegistrationRequestDTO; + +@ApplicationScoped +public class TkitPortalLogParam implements LogParam { + + @Override + public List getClasses() { + return List.of( + this.item(10, MenuRegistrationRequestDTO.class, + x -> "MenuRegistrationRequestDTO[ request version: " + + ((MenuRegistrationRequestDTO) x).getRequestVersion() + + "menu items size: " + + (((MenuRegistrationRequestDTO) x).getMenuItems() != null + ? ((MenuRegistrationRequestDTO) x).getMenuItems().size() + : "null") + + " ]")); + } + +} diff --git a/src/main/java/io/github/onecx/workspace/rs/legacy/mappers/PortalLegacyMapper.java b/src/main/java/io/github/onecx/workspace/rs/legacy/mappers/PortalLegacyMapper.java new file mode 100644 index 0000000..4feb33f --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/legacy/mappers/PortalLegacyMapper.java @@ -0,0 +1,67 @@ +package io.github.onecx.workspace.rs.legacy.mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ValueMapping; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.workspace.rs.legacy.model.MenuItemStructureDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.RestExceptionDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.ScopeDTO; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.domain.models.enums.Scope; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface PortalLegacyMapper { + + default List mapToTree(List menuItems) { + List result = new ArrayList<>(); + if (menuItems == null) { + return result; + } + + // create map of + var map = menuItems.stream().map(this::mapWithEmptyChildren) + .collect(Collectors.toMap(MenuItemStructureDTO::getId, x -> x)); + + // loop over all menu items, add parent or child in result tree + menuItems.forEach(m -> { + if (m.getParent() == null) { + result.add(map.get(m.getId())); + } else { + var parent = map.get(m.getParent().getId()); + parent.addChildrenItem(map.get(m.getId())); + } + }); + + return result; + } + + @Mapping(target = "portalExit", source = "workspaceExit") + @Mapping(target = "parentItemId", source = "parent.id") + @Mapping(target = "portalId", source = "workspaceName") + @Mapping(target = "parentKey", source = "parent.id") + @Mapping(target = "version", source = "modificationCount") + @Mapping(target = "removeChildrenItem", ignore = true) + @Mapping(target = "removeI18nItem", ignore = true) + @Mapping(target = "children", ignore = true) + MenuItemStructureDTO mapWithEmptyChildren(MenuItem entity); + + @ValueMapping(target = "PORTAL", source = "WORKSPACE") + ScopeDTO map(Scope scope); + + @Mapping(target = "removeParametersItem", ignore = true) + @Mapping(target = "namedParameters", ignore = true) + @Mapping(target = "removeNamedParametersItem", ignore = true) + @Mapping(target = "parameters", ignore = true) + RestExceptionDTO exception(String errorCode, String message); + + @Mapping(target = "removeParametersItem", ignore = true) + @Mapping(target = "namedParameters", ignore = true) + @Mapping(target = "removeNamedParametersItem", ignore = true) + RestExceptionDTO exception(String errorCode, String message, List parameters); +} diff --git a/src/main/java/io/github/onecx/workspace/rs/legacy/mappers/TkitPortalMapper.java b/src/main/java/io/github/onecx/workspace/rs/legacy/mappers/TkitPortalMapper.java new file mode 100644 index 0000000..8437607 --- /dev/null +++ b/src/main/java/io/github/onecx/workspace/rs/legacy/mappers/TkitPortalMapper.java @@ -0,0 +1,119 @@ +package io.github.onecx.workspace.rs.legacy.mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ValueMapping; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.workspace.rs.legacy.model.RestExceptionDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.ScopeDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.TkitMenuItemStructureDTO; +import io.github.onecx.workspace.domain.models.MenuItem; +import io.github.onecx.workspace.domain.models.Workspace; +import io.github.onecx.workspace.domain.models.enums.Scope; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface TkitPortalMapper { + + default void recursiveMappingTreeStructure(List items, Workspace workspace, MenuItem parent, + String applicationId, List mappedItems) { + int position = 0; + for (TkitMenuItemStructureDTO item : items) { + if (item != null) { + MenuItem menu = mapMenu(item); + updateMenu(menu, position, workspace, parent, applicationId); + mappedItems.add(menu); + position++; + + if (item.getChildren() == null || item.getChildren().isEmpty()) { + continue; + } + + recursiveMappingTreeStructure(item.getChildren(), workspace, menu, applicationId, mappedItems); + } + } + } + + @Mapping(target = "workspaceExit", source = "portalExit") + @Mapping(target = "roles", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "description", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "workspaceName", source = "portalId") + @Mapping(target = "children", ignore = true) + @Mapping(target = "modificationCount", ignore = true, defaultValue = "0") + @Mapping(target = "parent.id", source = "parentItemId") + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "workspace", ignore = true) + @Mapping(target = "permission", source = "permissionObject") + MenuItem mapMenu(TkitMenuItemStructureDTO menuItemStructureDto); + + default void updateMenu(MenuItem menuItem, int position, Workspace workspace, + MenuItem parent, String applicationId) { + menuItem.setWorkspace(workspace); + menuItem.setWorkspaceName(workspace.getWorkspaceName()); + menuItem.setPosition(position); + menuItem.setParent(parent); + menuItem.setApplicationId(applicationId); + } + + @ValueMapping(target = "WORKSPACE", source = "PORTAL") + Scope map(ScopeDTO scope); + + default List mapToTree(List menuItems) { + var result = new ArrayList(); + if (menuItems == null) { + return result; + } + + // create map of + var map = menuItems.stream().map(this::mapWithEmptyChildren) + .collect(Collectors.toMap(TkitMenuItemStructureDTO::getGuid, x -> x)); + + // loop over all menu items, add parent or child in result tree + menuItems.forEach(m -> { + if (m.getParent() == null) { + result.add(map.get(m.getId())); + } else { + var parent = map.get(m.getParent().getId()); + parent.addChildrenItem(map.get(m.getId())); + } + }); + + return result; + } + + @Mapping(target = "permissionObject", source = "permission") + @Mapping(target = "portalExit", source = "workspaceExit") + @Mapping(target = "parentItemId", source = "parent.id") + @Mapping(target = "portalId", source = "workspaceName") + @Mapping(target = "parentKey", source = "parent.id") + @Mapping(target = "version", source = "modificationCount") + @Mapping(target = "removeChildrenItem", ignore = true) + @Mapping(target = "removeI18nItem", ignore = true) + @Mapping(target = "children", ignore = true) + @Mapping(target = "guid", source = "id") + TkitMenuItemStructureDTO mapWithEmptyChildren(MenuItem entity); + + @ValueMapping(target = "PORTAL", source = "WORKSPACE") + ScopeDTO map(Scope scope); + + @Mapping(target = "removeParametersItem", ignore = true) + @Mapping(target = "namedParameters", ignore = true) + @Mapping(target = "removeNamedParametersItem", ignore = true) + @Mapping(target = "parameters", ignore = true) + RestExceptionDTO exception(String errorCode, String message); + + @Mapping(target = "removeParametersItem", ignore = true) + @Mapping(target = "namedParameters", ignore = true) + @Mapping(target = "removeNamedParametersItem", ignore = true) + RestExceptionDTO exception(String errorCode, String message, List parameters); +} diff --git a/src/main/openapi/di-workspace-v1.yaml b/src/main/openapi/di-workspace-v1.yaml new file mode 100644 index 0000000..611d824 --- /dev/null +++ b/src/main/openapi/di-workspace-v1.yaml @@ -0,0 +1,169 @@ +--- +openapi: 3.0.3 +info: + title: onecx-workspace data import workspace + version: 1.0.0 +servers: + - url: "http://localhost" +paths: + /import/workspace: + post: + operationId: importWorkspace + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceDataImport' + responses: + 200: + description: ok +components: + schemas: + WorkspaceDataImport: + type: object + properties: + requests: + type: array + items: + $ref: '#/components/schemas/ImportRequest' + ImportRequest: + required: + - workspace + type: object + properties: + workspace: + $ref: '#/components/schemas/WorkspaceImport' + menuItems: + description: Menu Items structure imported with a workspace + type: array + items: + $ref: '#/components/schemas/MenuItemStructure' + MenuItemStructure: + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + url: + type: string + disabled: + type: boolean + position: + format: int32 + type: integer + workspaceExit: + type: boolean + i18n: + type: object + additionalProperties: + type: string + children: + type: array + items: + $ref: '#/components/schemas/MenuItemStructure' + roles: + type: array + items: + type: string + WorkspaceImport: + description: Workspace data to be imported + required: + - workspaceName + type: object + properties: + workspaceName: + description: Name of the workspace that should be created/updated + minLength: 1 + type: string + tenantId: + description: "TenantId which will be set to Workspace, when superAdmin Role\ + \ is assigned" + type: string + themeName: + description: Theme name provided in a case when themeImportData is empty + type: string + description: + type: string + homePage: + type: string + baseUrl: + type: string + companyName: + type: string + address: + $ref: '#/components/schemas/WorkspaceAddress' + phoneNumber: + type: string + rssFeedUrl: + type: string + footerLabel: + type: string + subjectLinks: + maxItems: 3 + uniqueItems: true + type: array + items: + $ref: '#/components/schemas/SubjectLink' + products: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/Product" + imageUrls: + uniqueItems: true + type: array + items: + type: string + workspaceRoles: + type: array + items: + type: string + logoUrl: + type: string + OffsetDateTime: + format: date-time + type: string + example: 2022-03-10T12:15:50-04:00 + SubjectLink: + type: object + properties: + label: + type: string + url: + type: string + WorkspaceAddress: + type: object + properties: + street: + type: string + streetNo: + type: string + city: + type: string + country: + type: string + postalCode: + type: string + Product: + description: "Product to be imported" + type: object + properties: + productName: + type: string + baseUrl: + type: string + microfrontends: + type: array + items: + $ref: '#/components/schemas/Microfrontend' + Microfrontend: + description: "MFE defined under product for a path inside of the product" + type: object + properties: + mfeId: + type: string + basePath: + type: string \ No newline at end of file diff --git a/src/main/openapi/onecx-workspace-internal-openapi.yaml b/src/main/openapi/onecx-workspace-internal-openapi.yaml new file mode 100644 index 0000000..d3bcb9c --- /dev/null +++ b/src/main/openapi/onecx-workspace-internal-openapi.yaml @@ -0,0 +1,917 @@ +--- +openapi: 3.0.3 +info: + title: onecx-workspace internal service + version: 1.0.0 +servers: + - url: "http://onecx-workspace:8080" +tags: + - name: workspaceInternal + - name: menuInternal + - name: productInternal +paths: + /internal/workspaces: + post: + tags: + - workspaceInternal + description: Create new workspace + operationId: createWorkspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWorkspaceRequest' + responses: + "201": + description: New workspace created + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/Workspace' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspaces/search: + post: + tags: + - workspaceInternal + description: Search for workspaces by search criteria + operationId: searchWorkspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceSearchCriteria' + responses: + "200": + description: Corresponding workspaces + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspacePageResult' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspaces/{id}: + get: + tags: + - workspaceInternal + description: Return workspace by id + operationId: getWorkspace + parameters: + - $ref: '#/components/parameters/id' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Workspace' + "404": + description: Not found + put: + tags: + - workspaceInternal + description: Update workspace by ID + operationId: updateWorkspace + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWorkspaceRequest' + responses: + "204": + description: Workspaces updated + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: Workspace not found + delete: + tags: + - workspaceInternal + description: Delete workspace by ID + operationId: deleteWorkspace + parameters: + - $ref: '#/components/parameters/id' + responses: + "204": + description: No Content + "400": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspaces/{id}/menuItems: + get: + tags: + - menuInternal + description: Find all menu items belonging to a workspace + operationId: getMenuItemsForWorkspaceId + parameters: + - $ref: '#/components/parameters/id' + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MenuItem' + patch: + tags: + - menuInternal + description: Bulk update menu Items + operationId: patchMenuItems + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/MenuItem' + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MenuItem' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: Menu items not found + post: + tags: + - menuInternal + description: Add a new menu item to workspace menu + operationId: createMenuItemForWorkspace + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMenuItem' + responses: + "201": + description: OK + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/MenuItem' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + delete: + tags: + - menuInternal + description: Delete all menu items in workspace + operationId: deleteAllMenuItemsForWorkspace + parameters: + - $ref: '#/components/parameters/id' + responses: + "204": + description: No content + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspaces/{id}/menuItems/tree: + get: + tags: + - menuInternal + description: Fetch the menuItems of the workspace in the tree structure + operationId: getMenuStructureForWorkspaceId + parameters: + - $ref: '#/components/parameters/id' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceMenuItemStructrue' + "404": + description: Not found + /internal/workspaces/{id}/menuItems/tree/upload: + post: + tags: + - menuInternal + description: Upload the menu structure for workspace + operationId: uploadMenuStructureForWorkspaceId + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceMenuItemStructrue' + responses: + "201": + description: New workspace created + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspaces/{id}/menuItems/{menuItemId}: + get: + tags: + - menuInternal + description: Retrieve menu item detail info + operationId: getMenuItemById + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/menuItemId' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MenuItem' + "404": + description: Not found + delete: + tags: + - menuInternal + description: Delete a menuItem by the workspace id and the menuItemId + operationId: deleteMenuItemById + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/menuItemId' + responses: + "204": + description: OK + "400": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + put: + tags: + - menuInternal + description: Update an existing menu item + operationId: updateMenuItem + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/menuItemId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MenuItem' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MenuItem' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: Menu item not found + /internal/workspaces/{id}/products: + post: + tags: + - productInternal + description: Create/Register new product to workspace + operationId: createProductInWorkspace + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProductRequest' + responses: + "201": + description: Product added + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + get: + tags: + - productInternal + description: Find all products belonging to a workspace + operationId: getProductsForWorkspaceId + parameters: + - $ref: '#/components/parameters/id' + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Product' + "404": + description: Not found + /internal/workspaces/{id}/products/{productId}: + delete: + tags: + - productInternal + description: Delete a product by the workspace id and the productId + operationId: deleteProductById + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/productId' + responses: + "204": + description: OK + "400": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + put: + tags: + - productInternal + description: Update product and MFEs + operationId: updateProductById + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/productId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProductRequest' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: Product not found +components: + schemas: + WorkspaceSearchCriteria: + type: object + properties: + workspaceName: + type: string + themeName: + type: string + pageNumber: + format: int32 + description: The number of page. + default: 0 + type: integer + pageSize: + format: int32 + description: The size of page + default: 10 + type: integer + WorkspacePageResult: + type: object + properties: + totalElements: + format: int64 + description: The total elements in the resource. + type: integer + number: + format: int32 + type: integer + size: + format: int32 + type: integer + totalPages: + format: int64 + type: integer + stream: + type: array + items: + $ref: '#/components/schemas/Workspace' + CreateWorkspaceRequest: + required: + - workspaceName + type: object + properties: + workspaceName: + type: string + description: + type: string + theme: + type: string + homePage: + type: string + baseUrl: + type: string + companyName: + type: string + phoneNumber: + type: string + rssFeedUrl: + type: string + footerLabel: + type: string + workspaceRoles: + type: string + logoUrl: + type: string + address: + $ref: '#/components/schemas/WorkspaceAddress' + subjectLinks: + uniqueItems: true + maxItems: 3 + type: array + items: + $ref: '#/components/schemas/SubjectLink' + imageUrls: + uniqueItems: true + type: array + items: + type: string + UpdateWorkspaceRequest: + required: + - workspaceName + type: object + properties: + workspaceName: + type: string + description: + type: string + theme: + type: string + homePage: + type: string + baseUrl: + type: string + companyName: + type: string + phoneNumber: + type: string + rssFeedUrl: + type: string + footerLabel: + type: string + workspaceRoles: + type: string + logoUrl: + type: string + address: + $ref: '#/components/schemas/WorkspaceAddress' + subjectLinks: + uniqueItems: true + maxItems: 3 + type: array + items: + $ref: '#/components/schemas/SubjectLink' + imageUrls: + uniqueItems: true + type: array + items: + type: string + Workspace: + required: + - workspaceName + type: object + properties: + id: + type: string + version: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + workspaceName: + type: string + description: + type: string + theme: + type: string + homePage: + type: string + baseUrl: + type: string + companyName: + type: string + phoneNumber: + type: string + rssFeedUrl: + type: string + footerLabel: + type: string + workspaceRoles: + type: string + logoUrl: + type: string + address: + $ref: '#/components/schemas/WorkspaceAddress' + subjectLinks: + uniqueItems: true + maxItems: 3 + type: array + items: + $ref: '#/components/schemas/SubjectLink' + imageUrls: + uniqueItems: true + type: array + items: + type: string + WorkspaceAddress: + type: object + properties: + street: + type: string + streetNo: + type: string + city: + type: string + country: + type: string + postalCode: + type: string + SubjectLink: + type: object + properties: + label: + type: string + url: + type: string + WorkspaceMenuItemStructrue: + type: object + properties: + menuItems: + type: array + items: + $ref: '#/components/schemas/WorkspaceMenuItem' + WorkspaceMenuItem: + type: object + properties: + version: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + key: + type: string + name: + type: string + description: + type: string + url: + type: string + workspaceName: + type: string + applicationId: + type: string + disabled: + type: boolean + position: + format: int32 + type: integer + permission: + type: string + badge: + type: string + scope: + $ref: '#/components/schemas/Scope' + workspaceExit: + type: boolean + parentItemId: + type: string + i18n: + type: object + additionalProperties: + type: string + roles: + type: array + items: + type: string + children: + type: array + items: + $ref: '#/components/schemas/WorkspaceMenuItem' + MenuItem: + type: object + properties: + version: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + key: + type: string + name: + type: string + description: + type: string + url: + type: string + workspaceName: + type: string + applicationId: + type: string + disabled: + type: boolean + position: + format: int32 + type: integer + permission: + type: string + badge: + type: string + scope: + $ref: '#/components/schemas/Scope' + workspaceExit: + type: boolean + parentItemId: + type: string + i18n: + type: object + additionalProperties: + type: string + roles: + type: array + items: + type: string + CreateMenuItem: + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + url: + type: string + disabled: + type: boolean + position: + format: int32 + type: integer + permission: + type: string + badge: + type: string + scope: + $ref: '#/components/schemas/Scope' + workspaceExit: + type: boolean + parentItemId: + type: string + i18n: + type: object + additionalProperties: + type: string + roles: + type: array + items: + type: string + Scope: + enum: + - WORKSPACE + - APP + - PAGE + type: string + ProblemDetailResponse: + type: object + properties: + errorCode: + type: string + detail: + type: string + params: + type: array + items: + $ref: '#/components/schemas/ProblemDetailParam' + invalidParams: + type: array + items: + $ref: '#/components/schemas/ProblemDetailInvalidParam' + ProblemDetailParam: + type: object + properties: + key: + type: string + value: + type: string + ProblemDetailInvalidParam: + type: object + properties: + name: + type: string + message: + type: string + OffsetDateTime: + format: date-time + type: string + example: 2022-03-10T12:15:50-04:00 + CreateProductRequest: + required: + - productName + - baseUrl + type: object + properties: + productName: + type: string + baseUrl: + type: string + microfrontends: + type: array + items: + $ref: '#/components/schemas/CreateMicrofrontend' + CreateMicrofrontend: + required: + - mfeId + - basePath + type: object + properties: + mfeId: + type: string + basePath: + type: string + UpdateProductRequest: + required: + - baseUrl + type: object + properties: + baseUrl: + type: string + microfrontends: + type: array + items: + $ref: '#/components/schemas/UpdateMicrofrontend' + UpdateMicrofrontend: + required: + - mfeId + - basePath + type: object + properties: + mfeId: + type: string + basePath: + type: string + Product: + type: object + properties: + version: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + productName: + type: string + baseUrl: + type: string + microfrontends: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/Microfrontend' + Microfrontend: + type: object + properties: + id: + type: string + mfeId: + type: string + basePath: + type: string + parameters: + pageNumber: + in: query + name: pageNumber + required: false + schema: + type: integer + format: int32 + default: 0 + pageSize: + in: query + name: pageSize + required: false + schema: + type: integer + format: int32 + default: 100 + id: + in: path + name: id + required: true + schema: + type: string + menuItemId: + in: path + name: menuItemId + required: true + schema: + type: string + productId: + in: path + name: productId + required: true + schema: + type: string diff --git a/src/main/openapi/onecx-workspace-legacy-openapi.yaml b/src/main/openapi/onecx-workspace-legacy-openapi.yaml new file mode 100644 index 0000000..d34dc77 --- /dev/null +++ b/src/main/openapi/onecx-workspace-legacy-openapi.yaml @@ -0,0 +1,299 @@ +--- +openapi: 3.0.3 +info: + title: onecx-portal legacy service + version: 1.0.0 +servers: + - url: "http://onecx-portal:8080" +tags: + - name: portalLegacy + - name: tkitPortal +paths: + /legacy/menustructure/{portalId}/{applicationId}: + get: + tags: + - portalLegacy + description: Fetch the menuItems of the portal and application pair in the tree + structure + operationId: getMenuStructureForPortalIdAndApplicationId + parameters: + - name: applicationId + in: path + required: true + schema: + type: string + - name: portalId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MenuItemStructure' + "401": + description: Not authorized + "404": + description: Not found + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/RestException' + deprecated: true + /legacy/menustructure/{portalName}: + get: + tags: + - portalLegacy + description: Fetch the menuItems of the portal in the tree structure + operationId: getMenuStructureForPortalName + parameters: + - name: portalName + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MenuItemStructure' + "401": + description: Not authorized + "404": + description: Not found + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/RestException' + deprecated: true + /1000kit-portal-server/menustructure/{portalName}: + get: + tags: + - tkitPortal + description: Fetch the menuItems of the portal in the tree structure + operationId: getMenuStructureForTkitPortalName + parameters: + - name: portalName + in: path + required: true + schema: + type: string + - name: interpolate + in: query + required: false + schema: + type: boolean + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TkitMenuItemStructure' + "401": + description: Not authorized + "404": + description: Not found + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/RestException' + deprecated: true + /1000kit-portal-server/menustructure/{portalName}/{appId}: + post: + tags: + - tkitPortal + description: Register menu from the application + operationId: submitMenuRegistrationRequest + parameters: + - name: portalName + in: path + required: true + schema: + type: string + - name: appId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MenuRegistrationRequest' + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MenuRegistrationResponse' + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/RestException' +components: + schemas: + RestException: + type: object + properties: + errorCode: + type: string + message: + type: string + parameters: + type: array + items: + type: object + namedParameters: + type: object + additionalProperties: + type: object + TkitMenuItemStructure: + type: object + properties: + version: + format: int32 + type: integer + guid: + type: string + persisted: + type: boolean + key: + type: string + name: + type: string + url: + type: string + portalId: + type: string + applicationId: + type: string + disabled: + type: boolean + position: + format: int32 + type: integer + badge: + type: string + portalExit: + type: boolean + parentItemId: + type: string + scope: + $ref: '#/components/schemas/Scope' + i18n: + type: object + additionalProperties: + type: string + children: + type: array + items: + $ref: '#/components/schemas/TkitMenuItemStructure' + parentKey: + type: string + permissionObject: + type: string + MenuItemStructure: + type: object + properties: + version: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + key: + type: string + name: + type: string + description: + type: string + url: + type: string + portalId: + type: string + applicationId: + type: string + disabled: + type: boolean + position: + format: int32 + type: integer + badge: + type: string + portalExit: + type: boolean + parentItemId: + type: string + scope: + $ref: '#/components/schemas/Scope' + i18n: + type: object + additionalProperties: + type: string + children: + type: array + items: + $ref: '#/components/schemas/MenuItemStructure' + parentKey: + type: string + OffsetDateTime: + format: date-time + type: string + example: 2022-03-10T12:15:50-04:00 + Scope: + enum: + - PORTAL + - APP + - PAGE + type: string + MenuRegistrationRequest: + type: object + properties: + requestVersion: + type: integer + menuItems: + type: array + items: + $ref: '#/components/schemas/TkitMenuItemStructure' + MenuRegistrationResponse: + type: object + properties: + applicationId: + type: string + requestVersion: + type: integer + applied: + type: boolean + notice: + type: string diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..2594ee5 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,32 @@ +# DEFAULT +quarkus.datasource.db-kind=postgresql +quarkus.datasource.jdbc.max-size=30 +quarkus.datasource.jdbc.min-size=10 + +quarkus.banner.enabled=false +quarkus.hibernate-orm.database.generation=validate +#quarkus.flyway.migrate-at-start=true +#quarkus.flyway.validate-on-migrate=true +quarkus.liquibase.migrate-at-start=true +quarkus.liquibase.validate-on-migrate=true +tkit.log.json.enabled=true + +tkit.dataimport.enabled=false +tkit.dataimport.configurations.workspace.file=dev-data.import.json +tkit.dataimport.configurations.workspace.metadata.operation=CLEAN_INSERT +tkit.dataimport.configurations.workspace.enabled=false +tkit.dataimport.configurations.workspace.stop-at-error=true + + +# DEV +%dev.tkit.log.json.enabled=false + +# TEST +%test.tkit.log.json.enabled=false + +# PROD +%prod.quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://postgresdb:5432/onecx-workspace?sslmode=disable} +%prod.quarkus.datasource.username=${DB_USER:onecx-workspace} +%prod.quarkus.datasource.password=${DB_PWD:onecx-workspace} + +# PIPE CONFIG diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml new file mode 100644 index 0000000..3962d4b --- /dev/null +++ b/src/main/resources/db/changeLog.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/v1/2023-11-09-create-tables.xml b/src/main/resources/db/v1/2023-11-09-create-tables.xml new file mode 100644 index 0000000..5e06e40 --- /dev/null +++ b/src/main/resources/db/v1/2023-11-09-create-tables.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/v1/2023-11-09-data-import-log.xml b/src/main/resources/db/v1/2023-11-09-data-import-log.xml new file mode 100644 index 0000000..7132b55 --- /dev/null +++ b/src/main/resources/db/v1/2023-11-09-data-import-log.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/java/io/github/onecx/workspace/domain/daos/MenuItemDAOTest.java b/src/test/java/io/github/onecx/workspace/domain/daos/MenuItemDAOTest.java new file mode 100644 index 0000000..95bd1aa --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/domain/daos/MenuItemDAOTest.java @@ -0,0 +1,48 @@ +package io.github.onecx.workspace.domain.daos; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class MenuItemDAOTest { + @Inject + MenuItemDAO dao; + + @InjectMock + EntityManager em; + + @BeforeEach + void beforeAll() { + Mockito.when(em.getCriteriaBuilder()).thenThrow(new RuntimeException("Test technical error exception")); + } + + @Test + void methodExceptionTests() { + methodExceptionTests(() -> dao.deleteAllMenuItemsByWorkspaceId(null), + MenuItemDAO.ErrorKeys.ERROR_DELETE_ALL_MENU_ITEMS_BY_WORKSPACE_ID); + methodExceptionTests(() -> dao.updateMenuItems(null, null, null), MenuItemDAO.ErrorKeys.ERROR_UPDATE_MENU_ITEMS); + methodExceptionTests(() -> dao.loadAllMenuItemsByWorkspaceName(null), + MenuItemDAO.ErrorKeys.ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_NAME); + methodExceptionTests(() -> dao.loadAllMenuItemsByWorkspaceId(null), + MenuItemDAO.ErrorKeys.ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_ID); + methodExceptionTests(() -> dao.deleteAllMenuItemsByWorkspaceNameAndAppId(null, null), + MenuItemDAO.ErrorKeys.ERROR_DELETE_ALL_MENU_ITEMS_BY_WORKSPACE_NAME_AND_APP_ID); + methodExceptionTests(() -> dao.loadMenuItemByWorkspaceAndKey(null, null), + MenuItemDAO.ErrorKeys.ERROR_LOAD_ALL_MENU_ITEMS_BY_WORKSPACE_NAME); + } + + void methodExceptionTests(Executable fn, Enum key) { + var exc = Assertions.assertThrows(DAOException.class, fn); + Assertions.assertEquals(key, exc.key); + } +} diff --git a/src/test/java/io/github/onecx/workspace/domain/daos/ProductDAOTest.java b/src/test/java/io/github/onecx/workspace/domain/daos/ProductDAOTest.java new file mode 100644 index 0000000..b0c6330 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/domain/daos/ProductDAOTest.java @@ -0,0 +1,39 @@ +package io.github.onecx.workspace.domain.daos; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ProductDAOTest { + @Inject + ProductDAO dao; + + @InjectMock + EntityManager em; + + @BeforeEach + void beforeAll() { + Mockito.when(em.getCriteriaBuilder()).thenThrow(new RuntimeException("Test technical error exception")); + } + + @Test + void methodExceptionTests() { + methodExceptionTests(() -> dao.getProductsForWorkspaceId(null), + ProductDAO.ErrorKeys.ERROR_FIND_PRODUCTS_BY_WORKSPACE_ID); + } + + void methodExceptionTests(Executable fn, Enum key) { + var exc = Assertions.assertThrows(DAOException.class, fn); + Assertions.assertEquals(key, exc.key); + } +} diff --git a/src/test/java/io/github/onecx/workspace/domain/daos/WorkspaceDAOTest.java b/src/test/java/io/github/onecx/workspace/domain/daos/WorkspaceDAOTest.java new file mode 100644 index 0000000..8ec2d41 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/domain/daos/WorkspaceDAOTest.java @@ -0,0 +1,44 @@ +package io.github.onecx.workspace.domain.daos; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class WorkspaceDAOTest { + @Inject + WorkspaceDAO dao; + + @InjectMock + EntityManager em; + + @BeforeEach + void beforeAll() { + Mockito.when(em.getCriteriaBuilder()).thenThrow(new RuntimeException("Test technical error exception")); + } + + @Test + void methodExceptionTests() { + methodExceptionTests(() -> dao.loadByWorkspaceName(null), + WorkspaceDAO.ErrorKeys.ERROR_FIND_WORKSPACE_NAME); + methodExceptionTests(() -> dao.findBySearchCriteria(null), + WorkspaceDAO.ErrorKeys.ERROR_FIND_BY_CRITERIA); + methodExceptionTests(() -> dao.findByWorkspaceName(null), + WorkspaceDAO.ErrorKeys.ERROR_FIND_WORKSPACE_NAME); + } + + void methodExceptionTests(Executable fn, Enum key) { + var exc = Assertions.assertThrows(DAOException.class, fn); + Assertions.assertEquals(key, exc.key); + } + +} diff --git a/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceExceptionTest.java b/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceExceptionTest.java new file mode 100644 index 0000000..ddfaf1a --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceExceptionTest.java @@ -0,0 +1,120 @@ +package io.github.onecx.workspace.domain.di; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; + +import java.util.ArrayList; +import java.util.Map; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.dataimport.DataImportConfig; +import org.tkit.quarkus.test.WithDBData; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gen.io.github.onecx.workspace.di.workspace.v1.model.ImportRequestDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.WorkspaceDataImportDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.WorkspaceImportDTOV1; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.models.Workspace; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@WithDBData(value = "data/testdata-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class WorkspaceDataImportServiceExceptionTest extends AbstractTest { + + @InjectMock + WorkspaceDAO dao; + + @Inject + WorkspaceDataImportService service; + + @Inject + ObjectMapper mapper; + + @BeforeEach + void init() { + doThrow(RuntimeException.class).when(dao).create((Workspace) any()); + } + + @Test + void importDataDaoExceptionTest() { + var config = new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new WorkspaceDataImportDTOV1(); + data.setRequests(new ArrayList<>()); + ImportRequestDTOV1 importRequest = new ImportRequestDTOV1(); + data.getRequests().add(importRequest); + WorkspaceImportDTOV1 workspace = new WorkspaceImportDTOV1(); + importRequest.setWorkspace(workspace); + workspace.setWorkspaceName("test1"); + workspace.setBaseUrl("baseurl"); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }; + + assertThrows(WorkspaceDataImportService.ImportException.class, () -> service.importData(config)); + + var config2 = new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new WorkspaceDataImportDTOV1(); + data.setRequests(new ArrayList<>()); + ImportRequestDTOV1 importRequest = new ImportRequestDTOV1(); + data.getRequests().add(importRequest); + WorkspaceImportDTOV1 workspace = new WorkspaceImportDTOV1(); + importRequest.setWorkspace(workspace); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }; + + assertThrows(WorkspaceDataImportService.ImportException.class, () -> service.importData(config2)); + + var config3 = new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new WorkspaceDataImportDTOV1(); + data.setRequests(new ArrayList<>()); + ImportRequestDTOV1 importRequest = new ImportRequestDTOV1(); + data.getRequests().add(importRequest); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }; + + assertThrows(WorkspaceDataImportService.ImportException.class, () -> service.importData(config3)); + } +} diff --git a/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceFileTest.java b/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceFileTest.java new file mode 100644 index 0000000..852837c --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceFileTest.java @@ -0,0 +1,53 @@ +package io.github.onecx.workspace.domain.di; + +import java.util.Map; +import java.util.stream.Stream; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.models.Workspace; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@DisplayName("Portal data import test from example file") +@TestProfile(WorkspaceDataImportServiceFileTest.CustomProfile.class) +class WorkspaceDataImportServiceFileTest extends AbstractTest { + + @Inject + WorkspaceDAO workspaceDAO; + + @Test + @DisplayName("Import portal data from file") + void importDataFromFileTest() { + Stream result = workspaceDAO.findAll(); + Assertions.assertEquals(1, result.count()); + + } + + public static class CustomProfile implements QuarkusTestProfile { + + @Override + public String getConfigProfile() { + return "test"; + } + + @Override + public Map getConfigOverrides() { + return Map.of( + "tkit.dataimport.enabled", "true", + "tkit.dataimport.configurations.workspace.enabled", "true", + "tkit.dataimport.configurations.workspace.file", "./src/test/resources/import/workspace-import.json", + "tkit.dataimport.configurations.workspace.metadata.operation", "CLEAN_INSERT", + "tkit.dataimport.configurations.workspace.stop-at-error", "true"); + } + } + +} diff --git a/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceTest.java b/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceTest.java new file mode 100644 index 0000000..0497af8 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/domain/di/WorkspaceDataImportServiceTest.java @@ -0,0 +1,210 @@ +package io.github.onecx.workspace.domain.di; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Map; +import java.util.stream.Stream; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.dataimport.DataImportConfig; +import org.tkit.quarkus.test.WithDBData; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gen.io.github.onecx.workspace.di.workspace.v1.model.ImportRequestDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.WorkspaceDataImportDTOV1; +import gen.io.github.onecx.workspace.di.workspace.v1.model.WorkspaceImportDTOV1; +import io.github.onecx.workspace.domain.daos.WorkspaceDAO; +import io.github.onecx.workspace.domain.models.Workspace; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@WithDBData(value = "data/testdata-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class WorkspaceDataImportServiceTest extends AbstractTest { + + @Inject + WorkspaceDAO dao; + + @Inject + WorkspaceDataImportService service; + + @Inject + ObjectMapper mapper; + + @Test + void importDataTest() { + Workspace workspace = dao.loadByWorkspaceName("test01"); + Assertions.assertNotNull(workspace); + Assertions.assertNotNull(workspace.getProducts()); + Assertions.assertNotNull(workspace.getProducts().get(0).getMicrofrontends()); + + // test not existing workspace + workspace = dao.loadByWorkspaceName("does-not-exist"); + Assertions.assertNull(workspace); + + Stream result = dao.findAll(); + Assertions.assertEquals(3, result.count()); + } + + @Test + void importDataWithoutMenuItemsTest() { + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new WorkspaceDataImportDTOV1(); + data.setRequests(new ArrayList<>()); + ImportRequestDTOV1 importRequest = new ImportRequestDTOV1(); + data.getRequests().add(importRequest); + WorkspaceImportDTOV1 workspace = new WorkspaceImportDTOV1(); + importRequest.setWorkspace(workspace); + workspace.setWorkspaceName("test1"); + workspace.setBaseUrl("baseurl"); + importRequest.setMenuItems(null); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + var workspaces = dao.findAll().toList(); + assertThat(workspaces).hasSize(1); + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new WorkspaceDataImportDTOV1(); + data.setRequests(new ArrayList<>()); + ImportRequestDTOV1 importRequest = new ImportRequestDTOV1(); + data.getRequests().add(importRequest); + WorkspaceImportDTOV1 workspace = new WorkspaceImportDTOV1(); + importRequest.setWorkspace(workspace); + workspace.setWorkspaceName("test1"); + workspace.setBaseUrl("baseurl"); + importRequest.setMenuItems(new ArrayList<>()); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + workspaces = dao.findAll().toList(); + assertThat(workspaces).hasSize(1); + } + + @Test + void importDataNONETest() { + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "NONE"); + } + }); + + var workspaces = dao.findAll().toList(); + assertThat(workspaces).hasSize(3); + } + + @Test + void importDataNotSupportedTest() { + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CUSTOM_NOT_SUPPORTED"); + } + }); + + var workspaces = dao.findAll().toList(); + assertThat(workspaces).hasSize(3); + } + + @Test + void importEmptyDataTest() { + assertDoesNotThrow(() -> { + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + return mapper.writeValueAsBytes(null); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + return mapper.writeValueAsBytes(new WorkspaceDataImportDTOV1()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new WorkspaceDataImportDTOV1(); + data.setRequests(new ArrayList<>()); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + }); + + var config = new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + return new byte[] { 0 }; + } + }; + assertThrows(WorkspaceDataImportService.ImportException.class, () -> service.importData(config)); + } + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestControllerIT.java b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestControllerIT.java new file mode 100644 index 0000000..2e3aa77 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestControllerIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class MenuInternalRestControllerIT extends MenuInternalRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestControllerTest.java b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestControllerTest.java new file mode 100644 index 0000000..cb22feb --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/MenuInternalRestControllerTest.java @@ -0,0 +1,639 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.*; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +@TestHTTPEndpoint(MenuInternalRestController.class) +@WithDBData(value = "data/testdata-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +public class MenuInternalRestControllerTest extends AbstractTest { + + @Test + void getMenuItemsForWorkspaceIdTest() { + var dto = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .get() + .then() + .statusCode(OK.getStatusCode()) + .extract().as(List.class); + + assertThat(dto).isNotNull().isNotEmpty(); + assertThat(dto).hasSize(13); + } + + @Test + void deleteMenuItemByIdTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("menuItemId", "33-13") + .delete("{menuItemId}") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + var dto = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .get() + .then() + .statusCode(OK.getStatusCode()) + .extract().as(List.class); + + assertThat(dto).isNotNull().isNotEmpty(); + assertThat(dto).hasSize(12); + } + + @Test + void deleteAllMenuItemsForWorkspaceTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .delete() + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + var dto = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .get() + .then() + .statusCode(OK.getStatusCode()) + .extract().as(List.class); + + assertThat(dto).isNotNull().isEmpty(); + assertThat(dto).hasSize(0); + } + + @Test + void createMenuItemForWorkspaceTest() { + CreateMenuItemDTO menuItem = new CreateMenuItemDTO(); + menuItem.setName("menu"); + menuItem.setKey("test01_menu"); + menuItem.setDisabled(false); + menuItem.setRoles(List.of("Role1")); + menuItem.setParentItemId("44-1"); + menuItem.setI18n(Map.of("de", "Test DE Menu", "en", "Test EN Menu")); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .body(menuItem) + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().header(HttpHeaders.LOCATION); + + assertThat(uri).isNotNull(); + + var dto = given().when() + .contentType(APPLICATION_JSON) + .get(uri) + .then().statusCode(OK.getStatusCode()) + .extract().as(MenuItemDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getName()).isEqualTo(menuItem.getName()); + assertThat(dto.getDescription()).isEqualTo(menuItem.getDescription()); + } + + @Test + void addMenuItemForPortalDuplicateKeyTest() { + CreateMenuItemDTO menuItem = new CreateMenuItemDTO(); + menuItem.setName("menu"); + menuItem.setKey("PORTAL_MAIN_MENU"); + menuItem.setDisabled(false); + menuItem.setRoles(List.of("Role1")); + menuItem.setParentItemId("44-1"); + + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .body(menuItem) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + @DisplayName("Add menu item to the portal with without parent menu item") + void addMenuItemForPortalWithoutParentTest() { + + CreateMenuItemDTO menuItem = new CreateMenuItemDTO(); + menuItem.setName("menu"); + menuItem.setKey("test01_menu"); + menuItem.setDisabled(false); + menuItem.setRoles(List.of("Role1")); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .body(menuItem) + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().header(HttpHeaders.LOCATION); + + assertThat(uri).isNotNull(); + + var dto = given().when() + .contentType(APPLICATION_JSON) + .get(uri) + .then().statusCode(OK.getStatusCode()) + .extract().as(MenuItemDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getName()).isEqualTo(menuItem.getName()); + assertThat(dto.getDescription()).isEqualTo(menuItem.getDescription()); + } + + @Test + @DisplayName("Add menu item to the portal with wrong menu parent from another portal") + void addMenuItemForPortalParentFromAnotherPortalTest() { + + CreateMenuItemDTO menuItem = new CreateMenuItemDTO(); + menuItem.setName("menu"); + menuItem.setKey("test01_menu"); + menuItem.setDisabled(false); + menuItem.setRoles(List.of("Role1")); + menuItem.setParentItemId("33-6"); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .body(menuItem) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + @DisplayName("Add menu item to the portal with wrong menu parent id") + void addMenuItemForPortalWrongMenuParentIdTest() { + + CreateMenuItemDTO menuItem = new CreateMenuItemDTO(); + menuItem.setName("menu"); + menuItem.setKey("test01_menu"); + menuItem.setDisabled(false); + menuItem.setRoles(List.of("Role1")); + menuItem.setParentItemId("does-not-exists"); + + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .body(menuItem) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + @DisplayName("Add menu item to the portal which does not exists") + void addMenuItemForPortalWrongPortalIdTest() { + + CreateMenuItemDTO menuItem = new CreateMenuItemDTO(); + menuItem.setName("menu"); + menuItem.setKey("test01_menu"); + menuItem.setDisabled(false); + menuItem.setRoles(List.of("Role1")); + menuItem.setParentItemId("does-not-exists"); + + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "does-not-exists") + .body(menuItem) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + void getMenuItemByIdTest() { + var dto = given().when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("menuItemId", "33-6") + .get("{menuItemId}") + .then().statusCode(OK.getStatusCode()) + .extract().as(MenuItemDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getName()).isEqualTo("Portal Child 1"); + } + + @Test + void getMenuStructureForWorkspaceIdTest() { + var data = given() + .when() + .pathParam("id", "11-111") + .get("/tree") + .then() + .statusCode(OK.getStatusCode()) + .extract().body().as(WorkspaceMenuItemStructrueDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getMenuItems()).hasSize(5); + assertThat(countMenuItems(data.getMenuItems())).isEqualTo(13); + } + + private int countMenuItems(Collection menuItemDTOS) { + int count = 0; + for (WorkspaceMenuItemDTO item : menuItemDTOS) { + count++; + if (item.getChildren() != null && !item.getChildren().isEmpty()) { + count += countMenuItems(item.getChildren()); + } + } + + return count; + } + + @Test + void getMenuStructureForPortalIdDoesNotExistsTest() { + var data = given() + .when() + .pathParam("id", "does-not-exists") + .get("/tree") + .then().statusCode(OK.getStatusCode()) + .extract().body().as(WorkspaceMenuItemStructrueDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getMenuItems()).isEmpty(); + } + + @Test + void bulkPatchMenuItemsNoBodyTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .patch() + .then() + .statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + void bulkPatchMenuItemsEmptyListTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .body(List.of()) + .pathParam("id", "11-222") + .patch() + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void patchMenuItemsTest() { + var newRolesMenuItem = List.of("Role2"); + + var menuItemDetailsDTO = new MenuItemDTO(); + menuItemDetailsDTO.setId("44-1"); + menuItemDetailsDTO.setName("Test menu 44-1"); + menuItemDetailsDTO.setDisabled(false); + menuItemDetailsDTO.setRoles(newRolesMenuItem); + + var menuItemDetailsDTO1 = new MenuItemDTO(); + menuItemDetailsDTO1.setId("44-2"); + menuItemDetailsDTO1.setParentItemId("44-5"); + menuItemDetailsDTO1.setName("Test menu 44-2"); + menuItemDetailsDTO1.setDisabled(false); + menuItemDetailsDTO1.setRoles(newRolesMenuItem); + + var updatedData = given() + .when() + .contentType(APPLICATION_JSON) + .body(List.of(menuItemDetailsDTO, menuItemDetailsDTO1)) + .pathParam("id", "11-222") + .patch() + .then().statusCode(OK.getStatusCode()) + .extract() + .as(new TypeRef>() { + }); + + assertThat(updatedData).isNotNull().hasSize(2); + + updatedData.forEach(e -> { + switch (e.getName()) { + case "Test menu 44-1": + case "Test menu 44-2": + assertThat(e.getWorkspaceName()).isEqualTo("test02"); + break; + default: + assertThat(e.getWorkspaceName()).isNull(); + } + }); + } + + @Test + void bulkPatchMenuItemsNewMenuItemTest() { + + var menuItemDetailsDTO = new MenuItemDTO(); + menuItemDetailsDTO.setId("44-1"); + menuItemDetailsDTO.setName("Test menu 44-1"); + menuItemDetailsDTO.setDisabled(false); + menuItemDetailsDTO.setRoles(List.of("Role2")); + + var menuItemDetailsDTO1 = new MenuItemDTO(); + menuItemDetailsDTO1.setId("does-not-exists"); + menuItemDetailsDTO1.setParentItemId("44-5"); + menuItemDetailsDTO1.setName("Test menu 44-2"); + menuItemDetailsDTO1.setDisabled(false); + menuItemDetailsDTO1.setRoles(List.of("Role2")); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(List.of(menuItemDetailsDTO, menuItemDetailsDTO1)) + .pathParam("id", "11-222") + .patch() + .then().statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void bulkPatchMenuItemsParentToParentTest() { + + var menuItemDetailsDTO1 = new MenuItemDTO(); + menuItemDetailsDTO1.setId("44-5"); + menuItemDetailsDTO1.setParentItemId("44-5"); + menuItemDetailsDTO1.setName("Test menu 44-2"); + menuItemDetailsDTO1.setDisabled(false); + menuItemDetailsDTO1.setRoles(List.of("Role2")); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(List.of(menuItemDetailsDTO1)) + .pathParam("id", "11-222") + .patch() + .then().statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + void patchMenuItemDoesNotUpdateParentTest() { + + var request = new MenuItemDTO(); + request.setKey("Test menu"); + request.setDisabled(false); + request.setParentItemId("44-2"); + + // update menu item + var updatedData = given() + .when() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("id", "11-222") + .pathParam("menuItemId", "44-6") + .put("{menuItemId}") + .then().statusCode(OK.getStatusCode()) + .extract().as(MenuItemDTO.class); + + assertThat(updatedData).isNotNull(); + assertThat(updatedData.getKey()).isEqualTo(request.getKey()); + assertThat(updatedData.getWorkspaceName()).isEqualTo("test02"); + } + + static Stream inputParams() { + return Stream.of( + Arguments.of("22-111", "55-4", "55-6"), + Arguments.of("11-222", "44-6", "does-not-exists"), + Arguments.of("11-222", "44-6", "55-1")); + } + + @ParameterizedTest + @MethodSource("inputParams") + void patchMenuItemErrors(String portalId, String menuItemId, String parentItemId) { + var request = new MenuItemDTO(); + request.setKey("Test menu"); + request.setDisabled(false); + request.setParentItemId(parentItemId); + + // update menu item + given() + .when() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("id", portalId) + .pathParam("menuItemId", menuItemId) + .put("{menuItemId}") + .then().statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + void updateMenuItemTest() { + var request = new MenuItemDTO(); + request.setKey("Test menu"); + request.setDescription("New test menu description"); + request.setDisabled(false); + request.setParentItemId("44-1"); + + // update menu item + var updatedData = given() + .when() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("id", "11-222") + .pathParam("menuItemId", "44-6") + .put("{menuItemId}") + .then() + .statusCode(OK.getStatusCode()) + .extract().as(MenuItemDTO.class); + + assertThat(updatedData).isNotNull(); + assertThat(updatedData.getKey()).isEqualTo(request.getKey()); + assertThat(updatedData.getDescription()).isEqualTo(request.getDescription()); + assertThat(updatedData.getWorkspaceName()).isEqualTo("test02"); + + var dto = given().when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .pathParam("menuItemId", "44-6") + .get("{menuItemId}") + .then() + .statusCode(OK.getStatusCode()) + .extract().as(MenuItemDTO.class); + + assertThat(dto).isNotNull(); + assertThat(updatedData.getKey()).isEqualTo(request.getKey()); + assertThat(updatedData.getWorkspaceName()).isEqualTo("test02"); + assertThat(updatedData.getDescription()).isEqualTo(request.getDescription()); + assertThat(dto.getName()).isEqualTo("Portal Child 1"); + } + + @Test + void updateMenuItemNotExistsTest() { + var request = new MenuItemDTO(); + request.setKey("Test menu"); + request.setDescription("New test menu description"); + request.setDisabled(false); + request.setParentItemId("44-1"); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("id", "11-222") + .pathParam("menuItemId", "not-exists") + .put("{menuItemId}") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void uploadMenuStructureNoBodyTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .post("/tree/upload") + .then().statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + void uploadMenuStructureNoMenuItemsTest() { + + var menuStructureListDTO = new WorkspaceMenuItemStructrueDTO(); + menuStructureListDTO.setMenuItems(null); + + var error = given() + .when() + .body(menuStructureListDTO) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .post("/tree/upload") + .then().statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo("MENU_ITEMS_NULL"); + + menuStructureListDTO = new WorkspaceMenuItemStructrueDTO(); + menuStructureListDTO.setMenuItems(new ArrayList<>()); + + error = given() + .when() + .body(menuStructureListDTO) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .post("/tree/upload") + .then().statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo("MENU_ITEMS_NULL"); + } + + @Test + void uploadMenuStructurePortalDoesNotExistsTest() { + + var menuStructureListDTO = new WorkspaceMenuItemStructrueDTO(); + var menuItemStructureDTO = new WorkspaceMenuItemDTO(); + + menuItemStructureDTO.setKey("Test menu"); + menuItemStructureDTO.setDisabled(false); + menuItemStructureDTO.setParentItemId("44-1"); + + menuStructureListDTO.addMenuItemsItem(menuItemStructureDTO); + + var error = given() + .when() + .body(menuStructureListDTO) + .contentType(APPLICATION_JSON) + .pathParam("id", "does-not-exists") + .post("/tree/upload") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo("WORKSPACE_DOES_NOT_EXIST"); + } + + @Test + void uploadMenuStructureTest() { + + var menuStructureListDTO = new WorkspaceMenuItemStructrueDTO(); + var menuItemStructureDTO = new WorkspaceMenuItemDTO(); + + menuItemStructureDTO.setKey("Test menu"); + menuItemStructureDTO.setDisabled(false); + menuItemStructureDTO.setParentItemId("44-1"); + menuItemStructureDTO.setChildren(new ArrayList<>()); + + var menuItemStructureDTO1 = new WorkspaceMenuItemDTO(); + menuItemStructureDTO1.setKey("Sub menu"); + menuItemStructureDTO1.setDisabled(false); + menuItemStructureDTO1.setParentItemId("44-1"); + menuItemStructureDTO.addChildrenItem(menuItemStructureDTO1); + menuItemStructureDTO1 = new WorkspaceMenuItemDTO(); + menuItemStructureDTO1.setKey("Sub menu2"); + menuItemStructureDTO1.setDisabled(false); + menuItemStructureDTO1.setParentItemId("44-1"); + menuItemStructureDTO.addChildrenItem(menuItemStructureDTO1); + menuItemStructureDTO1 = new WorkspaceMenuItemDTO(); + menuItemStructureDTO1.setKey("Sub menu3"); + menuItemStructureDTO1.setDisabled(false); + menuItemStructureDTO1.setParentItemId("44-1"); + menuItemStructureDTO.addChildrenItem(menuItemStructureDTO1); + + menuStructureListDTO.addMenuItemsItem(menuItemStructureDTO); + + // update menu item + given() + .when() + .contentType(APPLICATION_JSON) + .body(menuStructureListDTO) + .pathParam("id", "11-222") + .post("/tree/upload") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + var data = given() + .when() + .pathParam("id", "11-222") + .get("/tree") + .then() + .statusCode(OK.getStatusCode()) + .extract().body().as(WorkspaceMenuItemStructrueDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getMenuItems()).hasSize(1); + assertThat(countMenuItems(data.getMenuItems())).isEqualTo(4); + } +} diff --git a/src/test/java/io/github/onecx/workspace/rs/internal/controllers/ProductRestControllerIT.java b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/ProductRestControllerIT.java new file mode 100644 index 0000000..1933f22 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/ProductRestControllerIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ProductRestControllerIT extends ProductRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/internal/controllers/ProductRestControllerTest.java b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/ProductRestControllerTest.java new file mode 100644 index 0000000..1882e10 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/ProductRestControllerTest.java @@ -0,0 +1,201 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.*; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +@TestHTTPEndpoint(ProductInternalRestController.class) +@WithDBData(value = "data/testdata-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +public class ProductRestControllerTest extends AbstractTest { + + @Test + void createProductInWorkspaceTest() { + var request = new CreateProductRequestDTO(); + request.setProductName("testProduct"); + request.setBaseUrl("/test"); + var mfe = new CreateMicrofrontendDTO(); + mfe.setMfeId("testMfe1"); + mfe.setBasePath("/testMfe1"); + request.addMicrofrontendsItem(mfe); + mfe = new CreateMicrofrontendDTO(); + mfe.setMfeId("testMfe2"); + mfe.setBasePath("/testMfe2"); + request.addMicrofrontendsItem(mfe); + + // test not existing workspace + var error = given() + .when() + .body(request) + .contentType(APPLICATION_JSON) + .pathParam("id", "does-not-exists") + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo("WORKSPACE_DOES_NOT_EXIST"); + + var dto = given() + .when() + .body(request) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().as(ProductDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getMicrofrontends()).hasSize(2); + + request.setMicrofrontends(null); + request.setProductName("testProduct1"); + request.setBaseUrl("/test1"); + dto = given() + .when() + .body(request) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().as(ProductDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getMicrofrontends()).isNull(); + } + + @Test + void deleteProductByIdTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("productId", "5678") + .delete("{productId}") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + var dto = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .get() + .then() + .statusCode(OK.getStatusCode()) + .extract().as(new TypeRef>() { + }); + + assertThat(dto).isNotNull().isNotEmpty(); + assertThat(dto).hasSize(1); + assertThat(dto.get(0).getMicrofrontends()).isNotEmpty(); + } + + @Test + void getProductsForWorkspaceIdTest() { + // not existing product + var response = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "does-not-exist") + .get() + .then() + .statusCode(OK.getStatusCode()) + .extract().as(new TypeRef>() { + }); + + assertThat(response).isNotNull(); + assertThat(response).isEmpty(); + + // existing product + var dto = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .get() + .then() + .statusCode(OK.getStatusCode()) + .extract().as(new TypeRef>() { + }); + + assertThat(dto).isNotNull().isNotEmpty(); + assertThat(dto).hasSize(2); + assertThat(dto.get(0).getMicrofrontends()).isNotEmpty(); + assertThat(dto.get(1).getMicrofrontends()).isNotEmpty(); + } + + @Test + void updateProductByIdTest() { + var request = new UpdateProductRequestDTO(); + request.setBaseUrl("/onecx-core"); + + // not sending request + var error = given() + .when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("productId", "does-not-exist") + .put("{productId}") + .then() + .statusCode(BAD_REQUEST.getStatusCode()); + + assertThat(error).isNotNull(); + + // not existing product + given() + .when() + .body(request) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("productId", "does-not-exist") + .put("{productId}") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + var dto = given() + .when() + .body(request) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("productId", "1234") + .put("{productId}") + .then() + .statusCode(OK.getStatusCode()) + .extract().as(ProductDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getMicrofrontends()).isNull(); + assertThat(dto.getBaseUrl()).isEqualTo(request.getBaseUrl()); + + request.setMicrofrontends(new ArrayList<>()); + dto = given() + .when() + .body(request) + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .pathParam("productId", "1234") + .put("{productId}") + .then() + .statusCode(OK.getStatusCode()) + .extract().as(ProductDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getMicrofrontends()).isEmpty(); + assertThat(dto.getBaseUrl()).isEqualTo(request.getBaseUrl()); + } +} diff --git a/src/test/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestControllerIT.java b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestControllerIT.java new file mode 100644 index 0000000..3b5edeb --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestControllerIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class WorkspaceInternalRestControllerIT extends WorkspaceInternalRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestControllerTest.java b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestControllerTest.java new file mode 100644 index 0000000..b44910e --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/internal/controllers/WorkspaceInternalRestControllerTest.java @@ -0,0 +1,254 @@ +package io.github.onecx.workspace.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.*; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(WorkspaceInternalRestController.class) +@WithDBData(value = "data/testdata-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +public class WorkspaceInternalRestControllerTest extends AbstractTest { + + @Test + void createWorkspaceTest() { + + // create workspace + var createWorkspaceDTO = new CreateWorkspaceRequestDTO(); + createWorkspaceDTO + .workspaceName("Workspace1") + .companyName("Company1") + .baseUrl("/work1"); + + var dto = given() + .when() + .contentType(APPLICATION_JSON) + .body(createWorkspaceDTO) + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().as(WorkspaceDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspaceName()).isNotNull().isEqualTo(createWorkspaceDTO.getWorkspaceName()); + assertThat(dto.getCompanyName()).isNotNull().isEqualTo(createWorkspaceDTO.getCompanyName()); + assertThat(dto.getBaseUrl()).isNotNull().isEqualTo(createWorkspaceDTO.getBaseUrl()); + + // create without body + var exception = given() + .when() + .contentType(APPLICATION_JSON) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("CONSTRAINT_VIOLATIONS"); + assertThat(exception.getDetail()).isEqualTo("createWorkspace.createWorkspaceRequestDTO: must not be null"); + + exception = given() + .when() + .contentType(APPLICATION_JSON) + .body(createWorkspaceDTO) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("PERSIST_ENTITY_FAILED"); + assertThat(exception.getDetail()).isEqualTo( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'workspace_base_url_key' Detail: Key (base_url)=(/work1) already exists.]"); + } + + @Test + void deleteWorkspace() { + given() + .contentType(APPLICATION_JSON) + .pathParam("id", "22-111") + .delete("{id}") + .then().statusCode(NO_CONTENT.getStatusCode()); + + given().contentType(APPLICATION_JSON) + .pathParam("id", "22-111") + .get("{id}") + .then().statusCode(NOT_FOUND.getStatusCode()); + + given() + .contentType(APPLICATION_JSON) + .pathParam("id", "22-111") + .delete("{id}") + .then().statusCode(NO_CONTENT.getStatusCode()); + } + + @Test + void getWorkspace() { + var dto = given() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-111") + .get("{id}") + .then() + .statusCode(OK.getStatusCode()) + .extract().as(WorkspaceDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspaceName()).isNotNull().isEqualTo("test01"); + assertThat(dto.getCompanyName()).isNotNull().isEqualTo("Company1"); + assertThat(dto.getBaseUrl()).isNotNull().isEqualTo("/company1"); + assertThat(dto.getAddress()).isNotNull(); + assertThat(dto.getAddress().getStreetNo()).isEqualTo("6"); + assertThat(dto.getImageUrls()).isNotEmpty(); + assertThat(dto.getSubjectLinks()).isNotEmpty(); + } + + @Test + void searchWorkspacesTest() { + var criteria = new WorkspaceSearchCriteriaDTO(); + + // empty criteria + var data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(3); + assertThat(data.getStream()).isNotNull().hasSize(3); + + criteria.setWorkspaceName("test01"); + criteria.setThemeName("11-111"); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(1); + assertThat(data.getStream()).isNotNull().hasSize(1); + + criteria.setWorkspaceName(""); + criteria.setThemeName(""); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(3); + assertThat(data.getStream()).isNotNull().hasSize(3); + + criteria.setWorkspaceName(" "); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(0); + assertThat(data.getStream()).isNotNull().hasSize(0); + } + + @Test + void updateWorkspaceTest() { + var response = given().when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .get("{id}") + .then().statusCode(OK.getStatusCode()) + .extract().as(WorkspaceDTO.class); + // update none existing workspace + given().when() + .contentType(APPLICATION_JSON) + .body(response) + .pathParam("id", "none-exists") + .put("{id}") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + // update workspace with already existing baseUrl for other workspace + response.setBaseUrl("/company3"); + var error = given().when() + .contentType(APPLICATION_JSON) + .body(response) + .pathParam("id", "11-222") + .put("{id}") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo("MERGE_ENTITY_FAILED"); + assertThat(error.getDetail()).isEqualTo( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'workspace_base_url_key' Detail: Key (base_url)=(/company3) already exists.]"); + + // normal update + response.setBaseUrl("/company2/updated"); + response.setCompanyName("Company 2 updated"); + response.setWorkspaceName("Workspace2Test"); + given().when() + .contentType(APPLICATION_JSON) + .body(response) + .pathParam("id", "11-222") + .put("{id}") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + var updatedResponse = given().when() + .contentType(APPLICATION_JSON) + .pathParam("id", "11-222") + .get("{id}") + .then().statusCode(OK.getStatusCode()) + .extract().as(WorkspaceDTO.class); + + assertThat(updatedResponse).isNotNull(); + assertThat(updatedResponse.getAddress()).isNotNull(); + assertThat(updatedResponse.getAddress().getStreetNo()).isEqualTo(response.getAddress().getStreetNo()); + assertThat(updatedResponse.getAddress().getStreet()).isEqualTo(response.getAddress().getStreet()); + assertThat(updatedResponse.getWorkspaceRoles()).isEqualTo(response.getWorkspaceRoles()); + assertThat(updatedResponse.getCompanyName()).isEqualTo(response.getCompanyName()); + assertThat(updatedResponse.getBaseUrl()).isEqualTo(response.getBaseUrl()); + } + + private UpdateWorkspaceRequestDTO createUpdateWorkspaceDTO() { + UpdateWorkspaceRequestDTO dto = new UpdateWorkspaceRequestDTO(); + + dto.setWorkspaceName("test-workspace-1"); + dto.setDescription("update workspace description"); + dto.setWorkspaceRoles(List.of("TestRole1", "TestRole2").toString()); + + return dto; + } +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerExceptionTest.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerExceptionTest.java new file mode 100644 index 0000000..4515d92 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerExceptionTest.java @@ -0,0 +1,61 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import static io.github.onecx.workspace.rs.legacy.controllers.PortalLegacyRestControllerExceptionTest.ErrorKey.ERROR_TEST; +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.notNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import gen.io.github.onecx.workspace.rs.legacy.model.RestExceptionDTO; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(PortalLegacyRestController.class) +public class PortalLegacyRestControllerExceptionTest extends AbstractTest { + + @InjectMock + MenuItemDAO dao; + + @BeforeEach + void beforeAll() { + Mockito.when(dao.loadAllMenuItemsByWorkspaceName((String) notNull())) + .thenThrow(new RuntimeException("Test technical error exception")) + .thenThrow(new DAOException(ERROR_TEST, new RuntimeException("Test"))); + } + + @Test + void getMenuStructureForNoPortalNameTest() { + + var exception = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "TEST_ERROR") + .get("{portalName}") + .then().statusCode(INTERNAL_SERVER_ERROR.getStatusCode()) + .extract().as(RestExceptionDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("UNDEFINED_ERROR_CODE"); + + exception = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "TEST_ERROR") + .get("{portalName}") + .then().statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(RestExceptionDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo(ERROR_TEST.name()); + } + + public enum ErrorKey { + ERROR_TEST; + } +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerIT.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerIT.java new file mode 100644 index 0000000..7142988 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class PortalLegacyRestControllerIT extends PortalLegacyRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerTest.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerTest.java new file mode 100644 index 0000000..b5edbd7 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/PortalLegacyRestControllerTest.java @@ -0,0 +1,79 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.*; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.internal.model.*; +import gen.io.github.onecx.workspace.rs.legacy.model.MenuItemStructureDTO; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +@TestHTTPEndpoint(PortalLegacyRestController.class) +public class PortalLegacyRestControllerTest extends AbstractTest { + + @Test + void getMenuStructureForNoPortalNameTest() { + + var data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "LEGAGY_WRONG_PORTAL_ID") + .get("{portalName}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isEmpty(); + } + + @Test + @WithDBData(value = "data/testdata-legacy.xml", deleteAfterTest = true, deleteBeforeInsert = true) + void getMenuStructureForPortalNameTest() { + + var data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "test01") + .get("{portalName}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + assertThat(data).hasSize(5); + } + + @Test + @WithDBData(value = "data/testdata-legacy.xml", deleteAfterTest = true, deleteBeforeInsert = true) + void getMenuStructureForPortalNameAndAppTest() { + + var data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "test01") + .pathParam("applicationId", "test01") + .get("{portalName}/{applicationId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + assertThat(data).hasSize(5); + } +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerExceptionTest.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerExceptionTest.java new file mode 100644 index 0000000..5c60811 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerExceptionTest.java @@ -0,0 +1,61 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import static io.github.onecx.workspace.rs.legacy.controllers.PortalLegacyRestControllerExceptionTest.ErrorKey.ERROR_TEST; +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.notNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import gen.io.github.onecx.workspace.rs.legacy.model.RestExceptionDTO; +import io.github.onecx.workspace.domain.daos.MenuItemDAO; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(TkitPortalRestController.class) +public class TkitPortalRestControllerExceptionTest extends AbstractTest { + + @InjectMock + MenuItemDAO dao; + + @BeforeEach + void beforeAll() { + Mockito.when(dao.loadAllMenuItemsByWorkspaceName((String) notNull())) + .thenThrow(new RuntimeException("Test technical error exception")) + .thenThrow(new DAOException(ERROR_TEST, new RuntimeException("Test"))); + } + + @Test + void getMenuStructureForNoPortalNameTest() { + var exception = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "TEST_ERROR") + .get() + .then().statusCode(INTERNAL_SERVER_ERROR.getStatusCode()) + .extract().as(RestExceptionDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("UNDEFINED_ERROR_CODE"); + + exception = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "TEST_ERROR") + .get() + .then().statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(RestExceptionDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo(ERROR_TEST.name()); + } + + public enum ErrorKey { + ERROR_TEST; + } +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerIT.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerIT.java new file mode 100644 index 0000000..05b0542 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class TkitPortalRestControllerIT extends TkitPortalRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerInterpolationTest.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerInterpolationTest.java new file mode 100644 index 0000000..f0910a8 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerInterpolationTest.java @@ -0,0 +1,66 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.OK; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.legacy.model.TkitMenuItemStructureDTO; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +@TestHTTPEndpoint(TkitPortalRestController.class) +public class TkitPortalRestControllerInterpolationTest extends AbstractTest { + + @Test + @WithDBData(value = "data/testdata-legacy.xml", deleteAfterTest = true, deleteBeforeInsert = true) + void getMenuStructureForPortalNameTest() { + + var data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "test01") + .queryParam("interpolate", Boolean.TRUE) + .get() + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + assertThat(data).hasSize(5); + assertThat(getUrlFromMenuItemByName(data, "PORTAL_CHILD_5")).isEqualTo("interpolated5"); + assertThat(getUrlFromMenuItemByName(data, "PORTAL_CHILD_6")).isEqualTo("interpolated6"); + assertThat(getUrlFromMenuItemByName(data, "PORTAL_CHILD_7")).isEqualTo("/interpolated7"); + assertThat(getUrlFromMenuItemByName(data, "PORTAL_CHILD_8")).isEqualTo("/interpolated8"); + } + + private String getUrlFromMenuItemByName(List menuList, String key) { + String foundurl; + for (TkitMenuItemStructureDTO menuItem : menuList) { + if (menuItem.getKey().equals(key)) { + foundurl = menuItem.getUrl(); + return foundurl; + } + + if (menuItem.getChildren() != null && !menuItem.getChildren().isEmpty()) { + foundurl = getUrlFromMenuItemByName(menuItem.getChildren(), key); + if (foundurl != null) { + return foundurl; + } + } + } + + return null; + } + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerTest.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerTest.java new file mode 100644 index 0000000..ab7223d --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalRestControllerTest.java @@ -0,0 +1,126 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.OK; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.legacy.model.*; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +@TestHTTPEndpoint(TkitPortalRestController.class) +public class TkitPortalRestControllerTest extends AbstractTest { + + @Test + void getMenuStructureForNoPortalNameTest() { + + var data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "LEGAGY_WRONG_PORTAL_ID") + .get() + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isEmpty(); + } + + @Test + @WithDBData(value = "data/testdata-legacy.xml", deleteAfterTest = true, deleteBeforeInsert = true) + void getMenuStructureForPortalNameTest() { + + var data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "test01") + .get() + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + assertThat(data).hasSize(5); + + data = given() + .contentType(APPLICATION_JSON) + .pathParam("portalName", "test01") + .queryParam("interpolate", Boolean.FALSE) + .get() + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(new TypeRef>() { + }); + + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + assertThat(data).hasSize(5); + } + + @Test + @WithDBData(value = "data/testdata-legacy.xml", deleteAfterTest = true, deleteBeforeInsert = true) + void submitMenuRegistrationRequestTest() { + var request = new MenuRegistrationRequestDTO(); + request.setRequestVersion(0); + var menuItems = new ArrayList(); + request.setMenuItems(menuItems); + var menuItem = new TkitMenuItemStructureDTO(); + menuItems.add(menuItem); + + menuItem.setKey("PARAMETERS_MANAGEMENT_UI_ROOT"); + menuItem.setName("Parameters management root menu item"); + menuItem.setUrl("/1000kit-parameters-ui"); + menuItem.setDisabled(false); + menuItem.setPosition(0); + menuItem.setScope(ScopeDTO.APP); + menuItem.setParentKey("PORTAL_MAIN_MENU"); + menuItem.setPermissionObject("PARAMETERS_MANAGEMENT_UI_ROOT"); + menuItem.setI18n(Map.of("de", "Parameters management", "en", "Parameters management")); + menuItem.setChildren(new ArrayList<>()); + + var subMenuItem = new TkitMenuItemStructureDTO(); + menuItem.getChildren().add(subMenuItem); + subMenuItem.setKey("PARAMETERS_MANAGEMENT_SEARCH"); + subMenuItem.setName("Parameters management search page menu item"); + subMenuItem.setUrl("/1000kit-parameters-ui/parameters/search"); + subMenuItem.setDisabled(false); + subMenuItem.setPosition(0); + subMenuItem.setScope(ScopeDTO.PAGE); + subMenuItem.setPortalExit(false); + subMenuItem.setParentKey("PARAMETERS_MANAGEMENT_UI_ROOT"); + subMenuItem.setPermissionObject("PARAMETER_SEARCH"); + subMenuItem.setI18n(Map.of("de", "Parameters Management Search", "en", "Parameters Management Search")); + + var response = given() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("portalName", "test03") + .pathParam("appId", "parameters-management-ui") + .post("{appId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(MenuRegistrationResponseDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getApplied()).isFalse(); + assertThat(response.getNotice()).isEqualTo("TKITPORTAL10003 Menu registration request has been ignored"); + } + +} diff --git a/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalSubmitMenuRequestTest.java b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalSubmitMenuRequestTest.java new file mode 100644 index 0000000..696eff5 --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/rs/legacy/controllers/TkitPortalSubmitMenuRequestTest.java @@ -0,0 +1,149 @@ +package io.github.onecx.workspace.rs.legacy.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.OK; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.workspace.rs.legacy.model.MenuRegistrationRequestDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.MenuRegistrationResponseDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.ScopeDTO; +import gen.io.github.onecx.workspace.rs.legacy.model.TkitMenuItemStructureDTO; +import io.github.onecx.workspace.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(TkitPortalSubmitMenuRequestTest.CustomProfile.class) +@TestHTTPEndpoint(TkitPortalRestController.class) +class TkitPortalSubmitMenuRequestTest extends AbstractTest { + + @Test + @WithDBData(value = "data/testdata-legacy.xml", deleteAfterTest = true, deleteBeforeInsert = true) + void submitMenuRegistrationRequestTest() { + var request = new MenuRegistrationRequestDTO(); + var response = given() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("portalName", "does-not-exist") + .pathParam("appId", "parameters-management-ui") + .post("{appId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(MenuRegistrationResponseDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getApplied()).isFalse(); + + response = given() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("portalName", "test03") + .pathParam("appId", "parameters-management-ui") + .post("{appId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(MenuRegistrationResponseDTO.class); + assertThat(response).isNotNull(); + assertThat(response.getApplied()).isFalse(); + + request.setMenuItems(new ArrayList<>()); + response = given() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("portalName", "test03") + .pathParam("appId", "parameters-management-ui") + .post("{appId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(MenuRegistrationResponseDTO.class); + assertThat(response).isNotNull(); + assertThat(response.getApplied()).isFalse(); + + request = new MenuRegistrationRequestDTO(); + request.setRequestVersion(0); + var menuItems = new ArrayList(); + request.setMenuItems(menuItems); + var menuItem = new TkitMenuItemStructureDTO(); + menuItems.add(menuItem); + + menuItem.setKey("PARAMETERS_MANAGEMENT_UI_ROOT"); + menuItem.setName("Parameters management root menu item"); + menuItem.setUrl("/1000kit-parameters-ui"); + menuItem.setDisabled(false); + menuItem.setPosition(0); + menuItem.setScope(ScopeDTO.APP); + menuItem.setParentKey("PORTAL_MAIN_MENU"); + menuItem.setPermissionObject("PARAMETERS_MANAGEMENT_UI_ROOT"); + menuItem.setI18n(Map.of("de", "Parameters management", "en", "Parameters management")); + menuItem.setChildren(new ArrayList<>()); + + var subMenuItem = new TkitMenuItemStructureDTO(); + menuItem.getChildren().add(subMenuItem); + subMenuItem.setKey("PARAMETERS_MANAGEMENT_SEARCH"); + subMenuItem.setName("Parameters management search page menu item"); + subMenuItem.setUrl("/1000kit-parameters-ui/parameters/search"); + subMenuItem.setDisabled(false); + subMenuItem.setPosition(0); + subMenuItem.setScope(ScopeDTO.PAGE); + subMenuItem.setPortalExit(false); + subMenuItem.setParentKey("PARAMETERS_MANAGEMENT_UI_ROOT"); + subMenuItem.setPermissionObject("PARAMETER_SEARCH"); + subMenuItem.setI18n(Map.of("de", "Parameters Management Search", "en", "Parameters Management Search")); + + response = given() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("portalName", "test03") + .pathParam("appId", "parameters-management-ui") + .post("{appId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(MenuRegistrationResponseDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getApplied()).isTrue(); + + request.getMenuItems().get(0).setParentKey(null); + response = given() + .contentType(APPLICATION_JSON) + .body(request) + .pathParam("portalName", "test03") + .pathParam("appId", "parameters-management-ui") + .post("{appId}") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(MenuRegistrationResponseDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getApplied()).isTrue(); + } + + public static class CustomProfile implements QuarkusTestProfile { + + @Override + public String getConfigProfile() { + return "test"; + } + + @Override + public Map getConfigOverrides() { + return Map.of( + "tkit.legacy.enable.menu.auto.registration", "true"); + } + } + +} diff --git a/src/test/java/io/github/onecx/workspace/test/AbstractTest.java b/src/test/java/io/github/onecx/workspace/test/AbstractTest.java new file mode 100644 index 0000000..073e54f --- /dev/null +++ b/src/test/java/io/github/onecx/workspace/test/AbstractTest.java @@ -0,0 +1,25 @@ +package io.github.onecx.workspace.test; + +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; +import static io.restassured.RestAssured.config; +import static io.restassured.config.ObjectMapperConfig.objectMapperConfig; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.restassured.config.RestAssuredConfig; + +@SuppressWarnings("java:S2187") +public class AbstractTest { + + static { + config = RestAssuredConfig.config().objectMapperConfig( + objectMapperConfig().jackson2ObjectMapperFactory( + (cls, charset) -> { + var objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.configure(WRITE_DATES_AS_TIMESTAMPS, false); + return objectMapper; + })); + } +} diff --git a/src/test/resources/data/testdata-external.xml b/src/test/resources/data/testdata-external.xml new file mode 100644 index 0000000..42a271f --- /dev/null +++ b/src/test/resources/data/testdata-external.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/data/testdata-internal.xml b/src/test/resources/data/testdata-internal.xml new file mode 100644 index 0000000..f19cb57 --- /dev/null +++ b/src/test/resources/data/testdata-internal.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/data/testdata-legacy.xml b/src/test/resources/data/testdata-legacy.xml new file mode 100644 index 0000000..e9058a5 --- /dev/null +++ b/src/test/resources/data/testdata-legacy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/import/tkit-portal-menu.json b/src/test/resources/import/tkit-portal-menu.json new file mode 100644 index 0000000..ba56bdc --- /dev/null +++ b/src/test/resources/import/tkit-portal-menu.json @@ -0,0 +1,37 @@ +{ + "requestVersion": 0, + "menuItems": [ + { + "key": "PARAMETERS_MANAGEMENT_UI_ROOT", + "name": "Parameters management root menu item", + "i18n": { + "de": "Parameters management", + "en": "Parameters management" + }, + "url": "/1000kit-parameters-ui", + "disabled": false, + "position": 0, + "scope": "APP", + "parentKey": "PORTAL_MAIN_MENU", + "permissionObject": "PARAMETERS_MANAGEMENT_UI_ROOT", + "children": [ + { + "key": "PARAMETERS_MANAGEMENT_SEARCH", + "name": "Parameters management search page menu item", + "i18n": { + "de": "Parameters Management Search", + "en": "Parameters Management Search" + }, + "url": "/1000kit-parameters-ui/parameters/search", + "disabled": false, + "position": 0, + "children": null, + "scope": "PAGE", + "portalExit": false, + "parentKey": "PARAMETERS_MANAGEMENT_UI_ROOT", + "permissionObject": "PARAMETER_SEARCH" + } + ] + } + ] +} diff --git a/src/test/resources/import/workspace-import.json b/src/test/resources/import/workspace-import.json new file mode 100644 index 0000000..f37853f --- /dev/null +++ b/src/test/resources/import/workspace-import.json @@ -0,0 +1,411 @@ +{ + "requests": [ + { + "menuItems": [ + { + "id": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "children": [ + { + "id": "3ee3b5cb-1757-4a8b-b83f-cadee6b3c30a", + "applicationId": "portal-welcome-ui", + "badge": "home", + "children": [], + "disabled": false, + "i18n": { + "de": "Portal Startseite", + "en": "Portal Home" + }, + "key": "CORE_WELCOME", + "name": "Welcome Page", + "parentItemId": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "parentKey": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "portalExit": false, + "portalId": "ADMIN", + "position": 0, + "scope": "WORKSPACE", + "url": "/admin/" + }, + { + "id": "156a8cff-f341-47bf-a1fc-dede091ee7c0", + "applicationId": "announcement-help-management-ui", + "badge": "calendar-plus", + "children": [ + { + "id": "3146bfd2-f232-4e5a-9ab6-c8c80a230309", + "applicationId": "announcement-help-management-ui", + "badge": "comment", + "children": null, + "disabled": false, + "i18n": { + "de": "Ankündigungen", + "en": "Announcements" + }, + "key": "CORE_AHM_MGMT_A", + "name": "Announcements", + "parentItemId": "156a8cff-f341-47bf-a1fc-dede091ee7c0", + "parentKey": "156a8cff-f341-47bf-a1fc-dede091ee7c0", + "portalExit": false, + "portalId": "ADMIN", + "position": 0, + "scope": "WORKSPACE", + "url": "/admin/ah-mgmt/announcements" + }, + { + "id": "42e77a69-ca69-4cd4-a9bd-efd8e2daf190", + "applicationId": "announcement-help-management-ui", + "badge": "question-circle", + "children": [], + "disabled": false, + "i18n": { + "de": "Hilfe Artikel", + "en": "Help Items" + }, + "key": "CORE_AHM_MGMT_HI", + "name": "Help Items", + "parentItemId": "156a8cff-f341-47bf-a1fc-dede091ee7c0", + "parentKey": "156a8cff-f341-47bf-a1fc-dede091ee7c0", + "portalExit": false, + "portalId": "ADMIN", + "position": 0, + "scope": "WORKSPACE", + "url": "/admin/ah-mgmt/helpitems/" + }, + null + ], + "disabled": false, + "i18n": { + "de": "Ankündigungs & Hilfe Verwaltung", + "en": "Announcement & Help Management" + }, + "key": "CORE_AHM_MGMT", + "name": "Announcement & Help Management", + "parentItemId": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "parentKey": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "portalExit": false, + "portalId": "ADMIN", + "position": 6, + "scope": "WORKSPACE", + "url": "/admin/ah-mgmt/" + }, + { + "id": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "applicationId": "", + "badge": "folder", + "children": [ + { + "id": "20a2207c-cd4b-48cf-90e7-bb8ad91428e3", + "applicationId": "tkit-menu-management-ui", + "badge": "sitemap", + "children": [], + "disabled": false, + "i18n": { + "de": "Menü Verwaltung", + "en": "Menu Management" + }, + "key": "CORE_MENU_MGMT", + "name": "Menu Management", + "parentItemId": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "parentKey": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "portalExit": false, + "portalId": "ADMIN", + "position": 3, + "scope": "WORKSPACE", + "url": "/admin/portal-mgmt/menu/" + }, + { + "id": "bbb93e3e-6743-46fa-b18d-134a664294e1", + "applicationId": "portal-theme-management-ui", + "badge": "palette", + "children": [], + "disabled": false, + "i18n": { + "de": "Thema Verwaltung", + "en": "Theme Management" + }, + "key": "CORE_THEME_MGMT", + "name": "Theme Management", + "parentItemId": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "parentKey": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "portalExit": false, + "portalId": "ADMIN", + "position": 0, + "scope": "WORKSPACE", + "url": "/admin/portal-mgmt/theme/" + }, + { + "id": "412810d4-31c9-424e-b843-70dd6f186f5a", + "applicationId": "tkit-menu-management-ui", + "badge": "ellipsis-h", + "children": [], + "disabled": false, + "i18n": { + "de": "Portal Verwaltung", + "en": "Portal Management" + }, + "key": "CORE_PORTAL_MGMT", + "name": "Portal Management", + "parentItemId": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "parentKey": "3c94b43b-3fdf-4ed6-89c5-6e6e215a022a", + "portalExit": false, + "portalId": "ADMIN", + "position": 2, + "scope": "WORKSPACE", + "url": "/admin/portal-mgmt/portal/" + } + ], + "disabled": false, + "i18n": { + "de": "Portal Verwaltung", + "en": "Portal Management" + }, + "key": "CORE_PORTAL_PARENT", + "name": "Portal", + "parentItemId": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "parentKey": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "portalExit": false, + "portalId": "ADMIN", + "position": 2, + "scope": "WORKSPACE", + "url": "" + }, + { + "id": "f8f7f3b7-7fca-40af-8396-8550e664e10e", + "applicationId": "", + "badge": "cog", + "children": [], + "disabled": false, + "i18n": { + "de": "MFE Konfiguration", + "en": "MFE Configuration" + }, + "key": "CORE_MFE_CONFIG", + "name": "MFE Config", + "parentItemId": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "parentKey": "b6da299d-f7da-4ba9-89c6-cdb976a998ab", + "portalExit": false, + "portalId": "ADMIN", + "position": 98, + "scope": "WORKSPACE", + "url": "/admin/config/" + } + ], + "disabled": false, + "i18n": {}, + "key": "PORTAL_MAIN_MENU", + "name": "Main Menu", + "portalExit": false, + "portalId": "ADMIN", + "position": 0, + "scope": "WORKSPACE" + }, + { + "id": "92037049-3b2a-4539-bb06-61b5e441474c", + "children": [ + { + "id": "f92d0652-36ac-4c24-8f62-b0ba4322148f", + "applicationId": "personal-info", + "badge": "user", + "children": [], + "disabled": false, + "i18n": { + "de": "Meine persönlichen Daten", + "en": "My Personal Info" + }, + "key": "PERSONAL_INFO", + "name": "Personal Info", + "parentItemId": "92037049-3b2a-4539-bb06-61b5e441474c", + "parentKey": "92037049-3b2a-4539-bb06-61b5e441474c", + "portalExit": false, + "portalId": "ADMIN", + "position": 1, + "scope": "WORKSPACE", + "url": "/admin/account/profile" + }, + { + "id": "31b33bf3-cb77-424b-8599-18b75ebe7eb6", + "applicationId": "account-settings", + "badge": "cog", + "children": [], + "disabled": false, + "i18n": { + "de": "Kontoeinstellungen", + "en": "Account Settings" + }, + "key": "ACCOUNT_SETTINGS", + "name": "Account Settings", + "parentItemId": "92037049-3b2a-4539-bb06-61b5e441474c", + "parentKey": "92037049-3b2a-4539-bb06-61b5e441474c", + "portalExit": false, + "portalId": "ADMIN", + "position": 2, + "scope": "WORKSPACE", + "url": "/admin/account/settings" + }, + { + "id": "2011a598-a82a-47c0-b4fa-8ae4aef9febc", + "applicationId": "change-password", + "badge": "key", + "children": [], + "disabled": false, + "i18n": { + "de": "Passwort ändern", + "en": "Change Password" + }, + "key": "CHANGE_PASSWORD", + "name": "Change Password", + "parentItemId": "92037049-3b2a-4539-bb06-61b5e441474c", + "parentKey": "92037049-3b2a-4539-bb06-61b5e441474c", + "portalExit": false, + "portalId": "ADMIN", + "position": 3, + "scope": "WORKSPACE", + "url": "/admin/account/change-password" + }, + { + "id": "eac723dd-5bed-4501-94a9-fed6b662dd2a", + "applicationId": "roles-and-perms", + "badge": "unlock", + "children": [], + "disabled": false, + "i18n": { + "de": "Rollen & Berechtigungen", + "en": "Roles & Permissions" + }, + "key": "ROLES_AND_PERMS", + "name": "Roles & Permissions", + "parentItemId": "92037049-3b2a-4539-bb06-61b5e441474c", + "parentKey": "92037049-3b2a-4539-bb06-61b5e441474c", + "portalExit": false, + "portalId": "ADMIN", + "position": 4, + "scope": "WORKSPACE", + "url": "/admin/account/roles-and-perms" + } + ], + "disabled": false, + "i18n": {}, + "key": "USER_PROFILE_MENU", + "name": "User Profile Menu", + "portalExit": false, + "portalId": "ADMIN", + "position": 5, + "scope": "WORKSPACE" + }, + { + "id": "6102484b-f7fc-465d-bca9-f2346539e507", + "children": [], + "disabled": false, + "i18n": {}, + "key": "PORTAL_ADMIN_MENU", + "name": "Admin Menu", + "portalExit": false, + "portalId": "ADMIN", + "position": 2, + "scope": "WORKSPACE" + }, + { + "id": "2bc91211-95df-4d2d-b56d-b9ece916d956", + "disabled": false, + "i18n": {}, + "key": "PORTAL_FOOTER_MENU", + "name": "Footer Menu", + "portalExit": false, + "portalId": "ADMIN", + "position": 2, + "scope": "WORKSPACE", + "children": [ + { + "children": [], + "disabled": false, + "i18n": { + "de": "Kontakt", + "en": "Contact" + }, + "key": "FOOTER_CONTACT", + "name": "Contact", + "parentItemId": "2bc91211-95df-4d2d-b56d-b9ece916d956", + "parentKey": "PORTAL_FOOTER_MENU", + "portalExit": false, + "portalId": "ADMIN", + "position": 1, + "scope": "WORKSPACE", + "url": "/contact" + }, + { + "children": [], + "disabled": false, + "i18n": { + "de": "Impressum", + "en": "Imprint" + }, + "key": "FOOTER_Impressum", + "name": "Impressum", + "parentItemId": "2bc91211-95df-4d2d-b56d-b9ece916d956", + "parentKey": "PORTAL_FOOTER_MENU", + "portalExit": false, + "portalId": "ADMIN", + "position": 2, + "scope": "WORKSPACE", + "url": "/Impressum" + } + ] + }, + { + "id": "d9363003-def6-4315-9684-fc514c5987d9", + "children": [], + "disabled": false, + "i18n": {}, + "key": "PORTAL_EXTERNAL_NAV", + "name": "External Navigation Menu", + "portalExit": false, + "portalId": "ADMIN", + "position": 4, + "scope": "WORKSPACE" + }, + { + "id": "fc7a48e1-18e7-44ab-9f90-673871c11e84", + "children": [], + "disabled": false, + "i18n": {}, + "key": "PORTAL_MENU_HOME", + "name": "Home Menu", + "portalExit": false, + "portalId": "ADMIN", + "position": 0, + "scope": "WORKSPACE", + "url": "/admin/" + } + ], + "workspace": { + "baseUrl": "/admin", + "companyName": "Company", + "description": "Portal zur Administration von Portalen", + "footerLabel": "Admin Portal", + "homePage": "/admin", + "workspaceName": "ADMIN", + "workspaceRoles": [ + "onecx-portal-user", + "onecx-portal-admin", + "onecx-portal-super-admin" + ], + "products": [ + { + "baseUrl" : "/core", + "productName": "onecx-core", + "microfrontends": [ + { + "mfeId": "menu", + "basePath": "/menu" + }, + { + "mfeId": "theme", + "basePath": "/theme" + } + ] + } + ], + "themeName": "Theme1" + } + } + ] +}