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,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"}
]