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..deb96b8
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,213 @@
+
+
+
+ 4.0.0
+
+
+ io.github.onecx
+ onecx-quarkus3-parent
+ 0.18.0
+
+
+ onecx-product-store-svc
+ 999-SNAPSHOT
+
+
+
+
+ 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
+
+
+
+
+ operator-product-v1
+
+ generate
+
+
+ src/main/openapi/onecx-product-store-operator-product-v1.yaml
+ gen.io.github.onecx.product.store.rs.operator.product.v1
+ gen.io.github.onecx.product.store.rs.operator.product.v1.model
+ DTOV1
+
+
+
+ operator-mfe-v1
+
+ generate
+
+
+ src/main/openapi/onecx-product-store-operator-mfe-v1.yaml
+ gen.io.github.onecx.product.store.rs.operator.mfe.v1
+ gen.io.github.onecx.product.store.rs.operator.mfe.v1.model
+ DTOV1
+
+
+
+ internal
+
+ generate
+
+
+ src/main/openapi/onecx-product-store-internal.yaml
+ gen.io.github.onecx.product.store.rs.internal
+ gen.io.github.onecx.product.store.rs.internal.model
+ DTO
+
+
+
+ external-v1
+
+ generate
+
+
+ src/main/openapi/onecx-product-store-v1.yaml
+ gen.io.github.onecx.product.store.rs.external.v1
+ gen.io.github.onecx.product.store.rs.external.v1.model
+ DTOV1
+
+
+
+
+
+
+
+
+
diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm
new file mode 100644
index 0000000..a554421
--- /dev/null
+++ b/src/main/docker/Dockerfile.jvm
@@ -0,0 +1,13 @@
+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..f566aab
--- /dev/null
+++ b/src/main/helm/Chart.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+name: onecx-product-store-svc
+version: 0.0.0
+appVersion: 0.0.0
+description: Onecx product store service
+keywords:
+ - tenant
+sources:
+ - https://github.com/onecx/onecx-product-store-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..add1755
--- /dev/null
+++ b/src/main/helm/values.yaml
@@ -0,0 +1,6 @@
+app:
+ image:
+ repository: "onecx/onecx-theme-svc"
+ tag: 999-SNAPSHOT
+ db:
+ enabled: true
\ No newline at end of file
diff --git a/src/main/java/io/github/onecx/product/store/domain/CommonLogParam.java b/src/main/java/io/github/onecx/product/store/domain/CommonLogParam.java
new file mode 100644
index 0000000..c15cda1
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/CommonLogParam.java
@@ -0,0 +1,22 @@
+package io.github.onecx.product.store.domain;
+
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.resteasy.reactive.RestResponse;
+import org.tkit.quarkus.log.cdi.LogParam;
+
+// FIXME: Move this to onecx-core library
+@ApplicationScoped
+public class CommonLogParam implements LogParam {
+
+ @Override
+ public List- getAssignableFrom() {
+ return List.of(
+ this.item(100, Response.class, x -> "Response:" + ((Response) x).getStatus()),
+ this.item(100, RestResponse.class, x -> "Response:" + ((RestResponse>) x).getStatus()),
+ this.item(100, Exception.class, x -> x.getClass().getSimpleName() + ":" + ((Exception) x).getMessage()));
+ }
+}
diff --git a/src/main/java/io/github/onecx/product/store/domain/criteria/MicrofrontendSearchCriteria.java b/src/main/java/io/github/onecx/product/store/domain/criteria/MicrofrontendSearchCriteria.java
new file mode 100644
index 0000000..bfc7c83
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/criteria/MicrofrontendSearchCriteria.java
@@ -0,0 +1,19 @@
+package io.github.onecx.product.store.domain.criteria;
+
+import io.quarkus.runtime.annotations.RegisterForReflection;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@RegisterForReflection
+public class MicrofrontendSearchCriteria {
+
+ private String displayName;
+
+ private String productName;
+
+ private Integer pageNumber;
+
+ private Integer pageSize;
+}
diff --git a/src/main/java/io/github/onecx/product/store/domain/criteria/ProductSearchCriteria.java b/src/main/java/io/github/onecx/product/store/domain/criteria/ProductSearchCriteria.java
new file mode 100644
index 0000000..84ec8c0
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/criteria/ProductSearchCriteria.java
@@ -0,0 +1,17 @@
+package io.github.onecx.product.store.domain.criteria;
+
+import io.quarkus.runtime.annotations.RegisterForReflection;
+import lombok.Getter;
+import lombok.Setter;
+
+@RegisterForReflection
+@Getter
+@Setter
+public class ProductSearchCriteria {
+
+ private String name;
+
+ private Integer pageNumber;
+
+ private Integer pageSize;
+}
diff --git a/src/main/java/io/github/onecx/product/store/domain/daos/MicrofrontendDAO.java b/src/main/java/io/github/onecx/product/store/domain/daos/MicrofrontendDAO.java
new file mode 100644
index 0000000..1cdb8e4
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/daos/MicrofrontendDAO.java
@@ -0,0 +1,75 @@
+package io.github.onecx.product.store.domain.daos;
+
+import java.util.stream.Stream;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.persistence.NoResultException;
+import jakarta.transaction.Transactional;
+
+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.product.store.domain.criteria.MicrofrontendSearchCriteria;
+import io.github.onecx.product.store.domain.models.Microfrontend;
+import io.github.onecx.product.store.domain.models.Microfrontend_;
+
+@ApplicationScoped
+@Transactional(Transactional.TxType.NOT_SUPPORTED)
+public class MicrofrontendDAO extends AbstractDAO {
+
+ public PageResult findMicrofrontendsByCriteria(MicrofrontendSearchCriteria criteria) {
+ try {
+ var cb = this.getEntityManager().getCriteriaBuilder();
+ var cq = cb.createQuery(Microfrontend.class);
+ var root = cq.from(Microfrontend.class);
+
+ if (criteria.getProductName() != null && !criteria.getProductName().isBlank()) {
+ cq.where(cb.like(root.get(Microfrontend_.PRODUCT_NAME), QueryCriteriaUtil.wildcard(criteria.getProductName())));
+ }
+
+ if (criteria.getDisplayName() != null && !criteria.getDisplayName().isBlank()) {
+ cq.where(cb.like(root.get(Microfrontend_.DISPLAY_NAME), QueryCriteriaUtil.wildcard(criteria.getDisplayName())));
+ }
+
+ return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult();
+ } catch (Exception ex) {
+ throw new DAOException(ErrorKeys.ERROR_FIND_MFE_BY_CRITERIA, ex);
+ }
+ }
+
+ public Microfrontend findByMfeId(String mfeId) {
+ try {
+ var cb = this.getEntityManager().getCriteriaBuilder();
+ var cq = cb.createQuery(Microfrontend.class);
+ var root = cq.from(Microfrontend.class);
+ cq.where(cb.equal(root.get(Microfrontend_.MFE_ID), mfeId));
+ return this.getEntityManager().createQuery(cq).getSingleResult();
+ } catch (NoResultException nre) {
+ return null;
+ } catch (Exception ex) {
+ throw new DAOException(ErrorKeys.ERROR_FIND_MFE_BY_ID, ex, mfeId);
+ }
+ }
+
+ public Stream findByProductName(String productName) {
+ try {
+ var cb = this.getEntityManager().getCriteriaBuilder();
+ var cq = cb.createQuery(Microfrontend.class);
+ var root = cq.from(Microfrontend.class);
+ cq.where(cb.equal(root.get(Microfrontend_.PRODUCT_NAME), productName));
+ return this.getEntityManager().createQuery(cq).getResultStream();
+ } catch (Exception ex) {
+ throw new DAOException(ErrorKeys.ERROR_FIND_MFES_BY_PRODUCT_NAME, ex, productName);
+ }
+ }
+
+ public enum ErrorKeys {
+
+ ERROR_FIND_MFE_BY_CRITERIA,
+ ERROR_FIND_MFES_BY_PRODUCT_NAME,
+ ERROR_FIND_MFE_BY_ID;
+ }
+}
diff --git a/src/main/java/io/github/onecx/product/store/domain/daos/ProductDAO.java b/src/main/java/io/github/onecx/product/store/domain/daos/ProductDAO.java
new file mode 100644
index 0000000..ca0aaca
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/daos/ProductDAO.java
@@ -0,0 +1,57 @@
+package io.github.onecx.product.store.domain.daos;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.persistence.NoResultException;
+import jakarta.transaction.Transactional;
+
+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.product.store.domain.criteria.ProductSearchCriteria;
+import io.github.onecx.product.store.domain.models.Product;
+import io.github.onecx.product.store.domain.models.Product_;
+
+@ApplicationScoped
+@Transactional(Transactional.TxType.NOT_SUPPORTED)
+public class ProductDAO extends AbstractDAO {
+
+ public PageResult findProductsByCriteria(ProductSearchCriteria criteria) {
+ try {
+ var cb = getEntityManager().getCriteriaBuilder();
+ var cq = cb.createQuery(Product.class);
+ var root = cq.from(Product.class);
+
+ if (criteria.getName() != null && !criteria.getName().isBlank()) {
+ cq.where(cb.like(root.get(Product_.NAME), QueryCriteriaUtil.wildcard(criteria.getName())));
+ }
+
+ return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult();
+ } catch (Exception ex) {
+ throw new DAOException(ErrorKeys.ERROR_FIND_PRODUCTS_BY_CRITERIA, ex);
+ }
+ }
+
+ public Product findProductByName(String productName) {
+ 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_.name), productName));
+ return this.getEntityManager().createQuery(cq).getSingleResult();
+ } catch (NoResultException nre) {
+ return null;
+ } catch (Exception ex) {
+ throw new DAOException(ErrorKeys.ERROR_FIND_PRODUCT_BY_NAME, ex, productName);
+ }
+ }
+
+ public enum ErrorKeys {
+
+ ERROR_FIND_PRODUCTS_BY_CRITERIA,
+
+ ERROR_FIND_PRODUCT_BY_NAME;
+ }
+}
diff --git a/src/main/java/io/github/onecx/product/store/domain/models/Microfrontend.java b/src/main/java/io/github/onecx/product/store/domain/models/Microfrontend.java
new file mode 100644
index 0000000..5cc68c1
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/models/Microfrontend.java
@@ -0,0 +1,79 @@
+package io.github.onecx.product.store.domain.models;
+
+import static jakarta.persistence.EnumType.STRING;
+
+import jakarta.persistence.*;
+
+import org.tkit.quarkus.jpa.models.TraceableEntity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "PS_MICROFRONTEND", uniqueConstraints = {
+ @UniqueConstraint(name = "PS_MICROFRONTEND_UNIQUE", columnNames = { "REMOTE_ENTRY", "REMOTE_BASE_URL",
+ "EXPOSED_MODULE" }),
+ @UniqueConstraint(name = "PS_MICROFRONTEND_MFE_ID", columnNames = { "PRODUCT_NAME", "MFE_ID" }),
+ @UniqueConstraint(name = "PS_MICROFRONTEND_MFE_ID", columnNames = { "PRODUCT_NAME", "BASE_PATH" })
+}, indexes = {
+ @Index(name = "PS_MICROFRONTEND_PRODUCT_NAME", columnList = "PRODUCT_NAME")
+})
+@SuppressWarnings("java:S2160")
+public class Microfrontend extends TraceableEntity {
+
+ @Column(name = "BASE_PATH")
+ private String basePath;
+
+ @Column(name = "MFE_ID")
+ private String mfeId;
+
+ @Column(name = "PRODUCT_NAME")
+ private String productName;
+
+ @Column(name = "REMOTE_ENTRY")
+ private String remoteEntry;
+
+ @Column(name = "REMOTE_NAME")
+ private String remoteName;
+
+ @Column(name = "EXPOSED_MODULE")
+ private String exposedModule;
+
+ @Column(name = "DISPLAY_NAME")
+ private String displayName;
+
+ @Column(name = "WC_TAG_NAME")
+ private String wcTagName;
+
+ @Column(name = "I18N_PATH")
+ private String i18nPath;
+
+ @Column(name = "CONTACT", columnDefinition = "TEXT")
+ private String contact;
+
+ @Column(name = "APP_ID")
+ private String appId;
+
+ @Column(name = "APP_VERSION")
+ private String appVersion;
+
+ @Column(name = "NOTE", columnDefinition = "TEXT")
+ private String note;
+
+ @Column(name = "REMOTE_BASE_URL")
+ private String remoteBaseUrl;
+
+ @Column(name = "MODULE_TYPE")
+ @Enumerated(STRING)
+ private ModuleType moduleType;
+
+ @Column(name = "OPERATOR", nullable = false)
+ private boolean operator;
+
+ public enum ModuleType {
+ ANGULAR,
+ WEBCOMPONENT
+ }
+}
diff --git a/src/main/java/io/github/onecx/product/store/domain/models/Product.java b/src/main/java/io/github/onecx/product/store/domain/models/Product.java
new file mode 100644
index 0000000..10d11b7
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/domain/models/Product.java
@@ -0,0 +1,34 @@
+package io.github.onecx.product.store.domain.models;
+
+import jakarta.persistence.*;
+
+import org.tkit.quarkus.jpa.models.TraceableEntity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "PS_PRODUCT", uniqueConstraints = {
+ @UniqueConstraint(name = "UI_PS_PRODUCT_NAME", columnNames = { "NAME" }),
+ @UniqueConstraint(name = "UI_PS_PRODUCT_BASE_PATH", columnNames = { "BASE_PATH" })
+})
+@SuppressWarnings("squid:S2160")
+public class Product extends TraceableEntity {
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Column(name = "DESCRIPTION")
+ private String description;
+
+ @Column(name = "IMAGE_URL")
+ private String imageUrl;
+
+ @Column(name = "BASE_PATH")
+ private String basePath;
+
+ @Column(name = "OPERATOR", nullable = false)
+ private boolean operator;
+}
diff --git a/src/main/java/io/github/onecx/product/store/rs/external/v1/controllers/ProductsRestControllerV1.java b/src/main/java/io/github/onecx/product/store/rs/external/v1/controllers/ProductsRestControllerV1.java
new file mode 100644
index 0000000..948a333
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/rs/external/v1/controllers/ProductsRestControllerV1.java
@@ -0,0 +1,65 @@
+package io.github.onecx.product.store.rs.external.v1.controllers;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.resteasy.reactive.RestResponse;
+import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
+import org.tkit.quarkus.log.cdi.LogService;
+
+import gen.io.github.onecx.product.store.rs.external.v1.ProductsApi;
+import gen.io.github.onecx.product.store.rs.external.v1.model.ProductSearchCriteriaDTOV1;
+import gen.io.github.onecx.product.store.rs.external.v1.model.RestExceptionDTOV1;
+import io.github.onecx.product.store.domain.daos.MicrofrontendDAO;
+import io.github.onecx.product.store.domain.daos.ProductDAO;
+import io.github.onecx.product.store.rs.external.v1.mappers.ExceptionMapperV1;
+import io.github.onecx.product.store.rs.external.v1.mappers.ProductMapperV1;
+
+@Path("/v1/products")
+@LogService
+@ApplicationScoped
+@Transactional(Transactional.TxType.NOT_SUPPORTED)
+public class ProductsRestControllerV1 implements ProductsApi {
+
+ @Inject
+ ExceptionMapperV1 exceptionMapper;
+
+ @Inject
+ ProductMapperV1 mapper;
+
+ @Inject
+ ProductDAO dao;
+
+ @Inject
+ MicrofrontendDAO microfrontendDAO;
+
+ @Override
+ public Response getProductByName(String name) {
+ var product = dao.findProductByName(name);
+ if (product == null) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+
+ var microfrontends = microfrontendDAO.findByProductName(product.getName());
+
+ var dto = mapper.map(product, microfrontends);
+
+ return Response.ok(dto).build();
+ }
+
+ @Override
+ public Response searchProducts(ProductSearchCriteriaDTOV1 productSearchCriteriaDTOV1) {
+ var criteria = mapper.map(productSearchCriteriaDTOV1);
+ var result = dao.findProductsByCriteria(criteria);
+ return Response.ok(mapper.mapPageResult(result)).build();
+ }
+
+ @ServerExceptionMapper
+ public RestResponse constraint(ConstraintViolationException ex) {
+ return exceptionMapper.constraint(ex);
+ }
+}
diff --git a/src/main/java/io/github/onecx/product/store/rs/external/v1/mappers/ExceptionMapperV1.java b/src/main/java/io/github/onecx/product/store/rs/external/v1/mappers/ExceptionMapperV1.java
new file mode 100644
index 0000000..54e1988
--- /dev/null
+++ b/src/main/java/io/github/onecx/product/store/rs/external/v1/mappers/ExceptionMapperV1.java
@@ -0,0 +1,56 @@
+package io.github.onecx.product.store.rs.external.v1.mappers;
+
+import java.util.List;
+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.log.cdi.LogService;
+import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper;
+
+import gen.io.github.onecx.product.store.rs.external.v1.model.*;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Mapper(uses = { OffsetDateTimeMapper.class })
+public abstract class ExceptionMapperV1 {
+
+ @LogService(log = false)
+ public RestResponse constraint(ConstraintViolationException ex) {
+ var dto = exception("CONSTRAINT_VIOLATIONS", ex.getMessage());
+ dto.setValidations(createErrorValidationResponse(ex.getConstraintViolations()));
+ return RestResponse.status(Response.Status.BAD_REQUEST, dto);
+ }
+
+ @Mapping(target = "removeParametersItem", ignore = true)
+ @Mapping(target = "namedParameters", ignore = true)
+ @Mapping(target = "removeNamedParametersItem", ignore = true)
+ @Mapping(target = "parameters", ignore = true)
+ @Mapping(target = "validations", ignore = true)
+ @Mapping(target = "removeValidationsItem", ignore = true)
+ public abstract RestExceptionDTOV1 exception(String errorCode, String message);
+
+ @Mapping(target = "removeParametersItem", ignore = true)
+ @Mapping(target = "namedParameters", ignore = true)
+ @Mapping(target = "removeNamedParametersItem", ignore = true)
+ @Mapping(target = "validations", ignore = true)
+ @Mapping(target = "removeValidationsItem", ignore = true)
+ public abstract RestExceptionDTOV1 exception(String errorCode, String message, List