Project init
All checks were successful
Maven build / build (push) Successful in 1m47s

This commit is contained in:
Guillaume Dugas
2025-09-09 10:00:01 +02:00
commit de339f9554
102 changed files with 3781 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-storage-postgres-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>semrack-storage-postgres-deployment</artifactId>
<name>Semrack Storage Postgres - Deployment</name>
<dependencies>
<dependency>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-core-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-panache-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql-deployment</artifactId>
</dependency>
<dependency>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-storage-postgres</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package fr.codeanddata.semrack.storage.postgres.deployment;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
class SemrackStoragePostgresProcessor {
private static final String FEATURE = "semrack-storage-postgres";
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(FEATURE);
}
}

View File

@@ -0,0 +1,23 @@
package fr.codeanddata.semrack.storage.postgres.test;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import io.quarkus.test.QuarkusDevModeTest;
public class SemrackStoragePostgresDevModeTest {
// Start hot reload (DevMode) test with your extension loaded
@RegisterExtension
static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class));
@Test
public void writeYourOwnDevModeTest() {
// Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information
Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName());
}
}

View File

@@ -0,0 +1,23 @@
package fr.codeanddata.semrack.storage.postgres.test;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import io.quarkus.test.QuarkusUnitTest;
public class SemrackStoragePostgresTest {
// Start unit test with your extension loaded
@RegisterExtension
static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class));
@Test
public void writeYourOwnUnitTest() {
// Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information
Assertions.assertTrue(true, "Add some assertions to " + getClass().getName());
}
}

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-storage-postgres-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>semrack-storage-postgres-integration-tests</artifactId>
<name>Semrack Storage Postgres - Integration Tests</name>
<properties>
<skipITs>true</skipITs>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-storage-postgres</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.codeanddata.semrack.storage.postgres.it;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/semrack-storage-postgres")
@ApplicationScoped
public class SemrackStoragePostgresResource {
// add some rest methods here
@GET
public String hello() {
return "Hello semrack-storage-postgres";
}
}

View File

@@ -0,0 +1,7 @@
package fr.codeanddata.semrack.storage.postgres.it;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class SemrackStoragePostgresResourceIT extends SemrackStoragePostgresResourceTest {
}

View File

@@ -0,0 +1,21 @@
package fr.codeanddata.semrack.storage.postgres.it;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class SemrackStoragePostgresResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/semrack-storage-postgres")
.then()
.statusCode(200)
.body(is("Hello semrack-storage-postgres"));
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>semrack-storage-postgres-parent</artifactId>
<name>Semrack Storage Postgres - Parent</name>
<packaging>pom</packaging>
<modules>
<module>deployment</module>
<module>runtime</module>
</modules>
</project>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-storage-postgres-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>semrack-storage-postgres</artifactId>
<name>Semrack Storage Postgres - Runtime</name>
<dependencies>
<dependency>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package fr.codeanddata.semrack.storage.postgres.dtos;
import lombok.*;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class InLookupParams {
String field;
List<String> values;
}

View File

@@ -0,0 +1,27 @@
package fr.codeanddata.semrack.storage.postgres.entities;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.io.Serializable;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "SemrackDocument")
@Table(name = "semrack_document")
public class SemrackDocumentEntity extends PanacheEntity implements Serializable {
@Column(unique = true, columnDefinition = "varchar(256)")
String uid;
@JdbcTypeCode(SqlTypes.JSON)
SemrackDocument document;
}

View File

@@ -0,0 +1,23 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
@Identifier("and")
@ApplicationScoped
public class AndLookup implements SemrackJpaLookupExpression<List<Map<String, Object>>> {
@Inject
SemrackLookupService lookupService;
@Override
public String apply(Object expressions) {
final List<Map<String, Object>> typedExpressions = convert(expressions);
return String.join(" AND ", typedExpressions.stream().map(lookupService::lookup).toList());
}
}

View File

@@ -0,0 +1,24 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@Identifier("contains")
@ApplicationScoped
public class ContainsLookup implements SemrackJpaLookupExpression<Object> {
@Inject
ObjectMapper objectMapper;
@Override
public String apply(Object params) {
try {
return "document @> '" + objectMapper.writeValueAsString(params) + "'";
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,31 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.exceptions.SemrackRuntimeException;
import fr.codeanddata.semrack.storage.postgres.dtos.InLookupParams;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Identifier("in")
@ApplicationScoped
public class InLookup implements SemrackJpaLookupExpression<InLookupParams> {
@Override
public String apply(Object params) {
final InLookupParams inLookupParams = convert(params, InLookupParams.class);
final List<String> fields = new ArrayList<>(Arrays.asList(inLookupParams.getField().split("\\.")));
if (fields.isEmpty()) {
throw new SemrackRuntimeException("Field expected");
}
String query = "->> '" + fields.removeLast() + "'";
if (!fields.isEmpty()) {
query = "-> '" + String.join("' -> '", fields) + "' " + query;
}
return "document " + query + " in " + "('" + String.join("', '", inLookupParams.getValues()) + "')";
}
}

View File

@@ -0,0 +1,23 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
@Identifier("or")
@ApplicationScoped
public class OrLookup implements SemrackJpaLookupExpression<List<Map<String, Object>>> {
@Inject
SemrackLookupService lookupService;
@Override
public String apply(Object expressions) {
final List<Map<String, Object>> typedExpressions = convert(expressions);
return String.join(" OR ", typedExpressions.stream().map(lookupService::lookup).toList());
}
}

View File

@@ -0,0 +1,6 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.SemrackLookupExpression;
public interface SemrackJpaLookupExpression<T> extends SemrackLookupExpression<T> {
}

View File

@@ -0,0 +1,168 @@
package fr.codeanddata.semrack.storage.postgres.storage;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.codeanddata.semrack.core.SemdocStorage;
import fr.codeanddata.semrack.core.enums.SemrackSortDirection;
import fr.codeanddata.semrack.core.models.*;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import fr.codeanddata.semrack.core.utils.UIDGenerator;
import fr.codeanddata.semrack.storage.postgres.entities.SemrackDocumentEntity;
import fr.codeanddata.semrack.storage.postgres.utils.PathMapper;
import io.quarkus.hibernate.reactive.panache.PanacheRepository;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.*;
@ApplicationScoped
public class SemdocJpaStorage implements SemdocStorage, PanacheRepository<SemrackDocumentEntity> {
@Inject
UIDGenerator generator;
@Inject
PathMapper pathMapper;
@Inject
SemrackLookupService lookupService;
@Inject
ObjectMapper objectMapper;
@Override
@WithSession
public Uni<SemrackDocument> readDocument(String uid) {
return find("uid = ?1", uid)
.firstResult().map(d -> Optional.ofNullable(d).map(SemrackDocumentEntity::getDocument).orElse(null));
}
@Override
@WithSession
public Uni<SearchResult> searchDocument(SearchRequest query) {
return search(query);
}
@Override
public Uni<Long> countDocuments(SearchRequest request) {
final String lookup = lookupService.lookup(request.getFilter());
final String whereClause = lookup.isEmpty() ? "" : " WHERE " + lookup;
return getSession()
.chain(s -> s.createNativeQuery("SELECT count(id) FROM semrack_document" + whereClause, Long.class).getSingleResultOrNull())
.map(count -> count == null ? 0 : count);
}
@Override
public Uni<Boolean> documentsExist(SearchRequest query) {
return countDocuments(query)
.map(c -> c > 0);
}
@WithTransaction
@Override
public Uni<SemrackDocument> storeDocument(SemrackDocument document) {
if (document.getUid() == null) {
return createDocument(document);
} else {
return find("uid = ?1", document.getUid())
.count()
.chain(n -> n == 0 ? createDocument(document) : updateDocument(document));
}
}
// TODO err : handle existing document
@WithTransaction
Uni<SemrackDocument> createDocument(SemrackDocument document) {
final String uid = document.getUid() == null ? generator.apply(document) : document.getUid();
document.setUid(uid);
final SemrackDocumentEntity entity = SemrackDocumentEntity.builder()
.uid(uid)
.document(document)
.build();
return persist(entity).map(SemrackDocumentEntity::getDocument);
}
// TODO err : document not exists
@WithTransaction
Uni<SemrackDocument> updateDocument(SemrackDocument document) {
return update("uid = ?1, document = ?2 WHERE uid = ?1", document.getUid(), document)
.map(x -> document);
}
Uni<SearchResult> search(SearchRequest request) {
final String baseQuery = buildBaseQuery(request);
final StringBuilder sorting = new StringBuilder();
if (request.getSort() != null && !request.getSort().isEmpty()) {
sorting.append(" ORDER BY ");
sorting.append(String.join(", ", request.getSort().stream().map(sort -> sort.getField() + " " + Optional.ofNullable(sort.getDirection()).orElse(SemrackSortDirection.asc).name()).toList()));
}
final StringBuilder paginate = new StringBuilder();
if (request.getPaginate() != null) {
final SemrackPagination pagination = request.getPaginate();
final Integer size = pagination.getSize() == null ? 200 : pagination.getSize();
final Integer page = pagination.getPage() == null ? 0 : pagination.getPage();
paginate.append(" LIMIT ").append(size).append(" OFFSET ").append(size * page);
}
return getSession()
.chain(s -> {
final SearchResult searchResult = SearchResult.builder().build();
return countDocuments(request)
.invoke(count -> searchResult.setPagination(PaginationInfo.builder()
.page(Optional.ofNullable(request.getPaginate()).map(SemrackPagination::getPage).orElse(0))
.size(Optional.ofNullable(request.getPaginate()).map(SemrackPagination::getSize).orElse(0))
.total(count)
.build()))
.call(() -> s
.createNativeQuery(baseQuery + sorting + paginate, Object.class).getResultList()
.invoke(results -> searchResult.setDocuments(project(request.getFields(), results))))
.map(count -> searchResult);
});
}
String buildBaseQuery(SearchRequest request) {
final String lookup = lookupService.lookup(request.getFilter());
final String whereClause = lookup.isEmpty() ? "" : " WHERE " + lookup;
if (request.getFields() == null || request.getFields().isEmpty()) {
return "SELECT document FROM semrack_document" + whereClause;
} else {
return "SELECT " + toJsonbPathExtract(request.getFields()) + " FROM semrack_document" + whereClause;
}
}
String toJsonbPathExtract(List<String> fields) {
return String.join(",", fields.stream().map(field -> {
final String serializedField = "'" + String.join("','", field.split("\\.")) + "'";
return "jsonb_extract_path(document, " + serializedField + ")";
}).toList());
}
List<SemrackDocument> project(List<String> fields, Object results) {
final List<?> projectedResults = objectMapper.convertValue(results, List.class);
if (fields == null || fields.isEmpty()) {
return projectedResults.stream().map(result -> objectMapper.convertValue(result, SemrackDocument.class)).toList();
} else {
return projectedResults.stream().map(result -> {
final List<Object> row = fields.size() == 1 ? List.of(result) : objectMapper.convertValue(result, new TypeReference<>() {
});
final List<Object> rowMut = new ArrayList<>(row);
final List<String> fieldCp = new ArrayList<>(fields);
final Map<String, Object> document = new HashMap<>();
while (!fieldCp.isEmpty() && !row.isEmpty()) {
final String field = fieldCp.removeFirst();
final Object value = rowMut.removeFirst();
pathMapper.map(field, document, value);
}
return objectMapper.convertValue(document, SemrackDocument.class);
}).toList();
}
}
}

View File

@@ -0,0 +1,44 @@
package fr.codeanddata.semrack.storage.postgres.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.*;
import java.util.stream.Stream;
@ApplicationScoped
public class PathMapper {
@Inject
ObjectMapper objectMapper;
public Map<String, Object> map(String path, Object value) {
return map(path, new HashMap<>(), value);
}
public Map<String, Object> map(String path, Map<String, Object> target, Object value) {
setValue(new ArrayList<>(Stream.of(path.split("\\.")).toList()), target, value);
return target;
}
void setValue(List<String> paths, Map<String, Object> container, Object value) {
if (!paths.isEmpty()) {
final String key = paths.removeFirst();
if (paths.isEmpty()) {
container.put(key, value);
return;
}
if (!container.containsKey(key) || !(container.get(key) instanceof Map)) {
container.put(key, new HashMap<>());
}
final Map<String, Object> target = objectMapper.convertValue(container.get(key), new TypeReference<>() {});
container.put(key, target);
setValue(paths, target, value);
}
}
}

View File

@@ -0,0 +1,9 @@
name: Semrack Storage Postgres
#description: Do something useful.
metadata:
# keywords:
# - semrack-storage-postgres
# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension
# categories:
# - "miscellaneous"
# status: "preview"

View File

@@ -0,0 +1,7 @@
quarkus.datasource.db-kind=postgresql
quarkus.flyway.enabled=true
quarkus.flyway.active=false
quarkus.flyway.migrate-at-start=true
quarkus.native.resources.includes=src/main/resources/db/migration/**
quarkus.native.additional-build-args=-H:SerializationConfigurationResources=serialization-config.json

View File

@@ -0,0 +1,10 @@
create sequence semrack_document_SEQ start with 1 increment by 50;
create table semrack_document
(
id bigint not null,
uid varchar(256) unique,
document jsonb,
primary key (id)
);
alter table semrack_document
add constraint UK307dbjrfh151l0r7bb36xdwew unique (uid);

View File

@@ -0,0 +1,3 @@
[
{"name": "java.lang.String"}
]

View File

@@ -0,0 +1,25 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.common.annotation.Identifier;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Map;
@QuarkusTest
class ContainsLookupTest {
@Inject
@Identifier("contains")
ContainsLookup subject;
@Test
void testLookup() {
final String lookup = subject.apply(Map.of("product", Map.of("sku", "xxx")));
final String expectedLookup = "document @> '{\"product\":{\"sku\":\"xxx\"}}'";
Assertions.assertEquals(expectedLookup, lookup);
}
}

View File

@@ -0,0 +1,43 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.storage.postgres.dtos.InLookupParams;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.common.annotation.Identifier;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
@QuarkusTest
class InLookupTest {
@Inject
@Identifier("in")
InLookup subject;
@Test
void testConversion() {
final String field = "root.parent.child.field";
final List<String> values = List.of("un", "deux", "trois");
final Map<String, Object> inParams = Map.of(
"field", field,
"values", values
);
final InLookupParams convertedParams = subject.convert(inParams, InLookupParams.class);
Assertions.assertEquals(field, convertedParams.getField());
Assertions.assertEquals(values, convertedParams.getValues());
}
@Test
void testLookup() {
final String lookup = subject.apply(Map.of(
"field", "lookup.field",
"values", List.of("list", "of", "values")));
final String expectedLookup = "document -> 'lookup' ->> 'field' in ('list', 'of', 'values')";
Assertions.assertEquals(expectedLookup, lookup);
}
}

View File

@@ -0,0 +1,42 @@
package fr.codeanddata.semrack.storage.postgres.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
@QuarkusTest
public class PathMapperTest {
@Inject
PathMapper pathMapper;
@Inject
ObjectMapper objectMapper;
@Test
void testMapper() {
final Map<String, Object> map = new HashMap<>();
final Map<String, Object> root = new HashMap<>();
map.put("root", root);
root.put("name", "bob");
pathMapper.map("root.child.subchild1.value", map, 1);
pathMapper.map("root.child.subchild2.value", map, 2);
pathMapper.map("hello", map, "world");
Assertions.assertEquals("world", map.get("hello"));
Assertions.assertEquals(1, conv(conv(conv(map, "root"), "child"), "subchild1").get("value"));
Assertions.assertEquals(2, conv(conv(conv(map, "root"), "child"), "subchild2").get("value"));
Assertions.assertEquals("bob", conv(map, "root").get( "name"));
}
Map<String, Object> conv(Map<String, Object> container, String key) {
return objectMapper.convertValue(container.get(key), new TypeReference<>() {});
}
}