Add fields API
Some checks failed
Maven build / build (push) Failing after 1m14s

This commit is contained in:
Guillaume Dugas
2026-02-17 16:19:33 +01:00
parent def2e4140f
commit cdbe2dfd67
57 changed files with 537 additions and 497 deletions

View File

@@ -45,6 +45,16 @@
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-vertx</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>fr.codeanddata.semrack</groupId>
<artifactId>semrack-core-testing</artifactId>

View File

@@ -0,0 +1,73 @@
package fr.codeanddata.semrack.storage.postgres.storage;
import fr.codeanddata.semrack.core.models.StorageGet;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.util.Map;
@QuarkusTest
public class JpaStorageTest {
@Inject
JpaStorage jpaStorage;
@Test
@RunOnVertxContext
void testJpaStorageGetDefault(UniAsserter asserter) {
asserter.assertThat(
() -> Panache.withSession(() -> jpaStorage.get("doc-001", StorageGet.builder().build())),
doc -> {
assert doc != null;
assert "doc-001".equals(doc.getUid());
assert doc.getAnnotations() == null;
assert doc.getMetadata() == null;
assert doc.getFields() == null;
}
);
}
@Test
@RunOnVertxContext
void testJpaStorageGetWithAnnotationsAndMetadata(UniAsserter asserter) {
asserter.assertThat(
() -> Panache.withSession(() -> jpaStorage.get("doc-001", StorageGet.builder()
.annotationsSource(true)
.metadataSource(true)
.build())),
doc -> {
assert doc != null;
assert doc.getAnnotations() != null;
assert doc.getMetadata() != null;
}
);
}
@Test
@RunOnVertxContext
void testJpaStorageGetWithFields(UniAsserter asserter) {
asserter.assertThat(
() -> Panache.withSession(() -> jpaStorage.get("doc-001", StorageGet.builder()
.fields(Map.of(
"title", "title",
"content", "content",
"sku", "product.sku"
))
.build())),
doc -> {
assert doc != null;
assert doc.getFields() != null;
assert doc.getFields().containsKey("title");
assert "Test Document".equals(doc.getFields().get("title"));
assert doc.getFields().containsKey("content");
assert "Hello World".equals(doc.getFields().get("content"));
assert doc.getFields().containsKey("sku");
assert "001".equals(doc.getFields().get("sku"));
}
);
}
}

View File

@@ -1,23 +0,0 @@
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

@@ -1,23 +0,0 @@
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,3 @@
quarkus.flyway.enabled=false
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql

View File

@@ -0,0 +1,5 @@
INSERT INTO semrack_document (id, uid, document)
VALUES (nextval('semrack_document_SEQ'), 'doc-001',
'{"uid":"doc-001","annotations":{"category":"test"},"metadata":{"product":{"sku":"001"},"title":"Test Document","content":"Hello World"}}'),
(nextval('semrack_document_SEQ'), 'doc-002',
'{"uid":"doc-002","annotations":{"category":"test"},"metadata":{"product":{"sku":"002"},"title":"Test Document 2","content":"Hello World 2"}}');

View File

@@ -1,6 +1,6 @@
package fr.codeanddata.semrack.storage.postgres.entities;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import fr.codeanddata.semrack.core.models.Document;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -23,5 +23,5 @@ public class SemrackDocumentEntity extends PanacheEntity implements Serializable
String uid;
@JdbcTypeCode(SqlTypes.JSON)
SemrackDocument document;
Document document;
}

View File

@@ -0,0 +1,148 @@
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.Storage;
import fr.codeanddata.semrack.core.models.Document;
import fr.codeanddata.semrack.core.models.StorageGet;
import fr.codeanddata.semrack.core.utils.UIDGenerator;
import fr.codeanddata.semrack.storage.postgres.entities.SemrackDocumentEntity;
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 JpaStorage implements Storage, PanacheRepository<SemrackDocumentEntity> {
@Inject
UIDGenerator generator;
@Inject
ObjectMapper objectMapper;
@Override
@WithSession
public Uni<Document> get(String uid, StorageGet request) {
final List<String> fields = selectFields(request);
return getSession()
.chain(session -> session.createNativeQuery("select " + String.join(",", fields) + " from semrack_document where uid = ?1")
.setParameter(1, uid)
.getSingleResult())
.map(result -> mapDocument(result, request));
}
@Override
@WithSession
public Uni<List<Document>> get(List<String> uids, StorageGet request) {
final List<String> fields = selectFields(request);
return getSession()
.chain(session -> session.createNativeQuery("select " + String.join(",", fields) + " from semrack_document where uid in ?1")
.setParameter(1, uids)
.getResultList())
.map(results -> mapDocuments(results, request));
}
@WithTransaction
@Override
public Uni<Document> storeDocument(Document document) {
if (document.getUid() == null) {
return createDocument(document)
.call(this::flush);
} else {
return find("uid = ?1", document.getUid())
.count()
.chain(n -> n == 0 ? createDocument(document) : updateDocument(document))
.call(this::flush);
}
}
// TODO err : handle existing document
@WithTransaction
Uni<Document> createDocument(Document 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<Document> updateDocument(Document document) {
return update("uid = ?1, document = ?2 WHERE uid = ?1", document.getUid(), document)
.map(x -> document);
}
List<String> selectFields(StorageGet request) {
final List<String> fields = new ArrayList<>(List.of(
"uid as _uid",
(Optional.ofNullable(request.getAnnotationsSource()).orElse(false) ? "document->'annotations'" : "null") + " as _annotations",
(Optional.ofNullable(request.getMetadataSource()).orElse(false) ? "document->'metadata'" : "null") + " as _metadata"
));
if (request.getFields() != null) {
int i = 0;
for (String field : request.getFields().values()) {
fields.add("jsonb_path_query(document, '$.metadata." + field + "') as field_" + i++);
}
}
return fields;
}
Document mapDocument(Object result, StorageGet request) {
final Map<String, Integer> fieldMap = buildFieldMap(request);
final List<Object> resultList = objectMapper.convertValue(result, new TypeReference<>() {
});
final Object oAnnotations = resultList.get(fieldMap.get("_annotations"));
final Object oMetadata = resultList.get(fieldMap.get("_metadata"));
final Map<String, Object> annotations = oAnnotations == null ? null : objectMapper.convertValue(oAnnotations, new TypeReference<>() {
});
final Map<String, Object> metadata = oMetadata == null ? null : objectMapper.convertValue(oMetadata, new TypeReference<>() {
});
final Map<String, Object> fields = new HashMap<>();
if (request.getFields() != null) {
for (final String field : request.getFields().keySet()) {
fields.put(field, resultList.get(fieldMap.get(field)));
}
}
return Document.builder()
.uid((String) resultList.get(fieldMap.get("_uid")))
.annotations(annotations)
.metadata(metadata)
.fields(fields.isEmpty() ? null : fields)
.build();
}
List<Document> mapDocuments(List<Object> results, StorageGet request) {
return results.stream().map(doc -> mapDocument(doc, request)).toList();
}
Map<String, Integer> buildFieldMap(StorageGet request) {
final Map<String, Integer> fieldsMap = new HashMap<>(Map.of(
"_uid", 0,
"_annotations", 1,
"_metadata", 2
));
if (request.getFields() != null) {
Integer i = 3;
for (String field : request.getFields().keySet()) {
fieldsMap.put(field, i);
i++;
}
}
return fieldsMap;
}
}

View File

@@ -1,144 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.storage;
import fr.codeanddata.semrack.core.SemdocStorage;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import fr.codeanddata.semrack.core.utils.UIDGenerator;
import fr.codeanddata.semrack.storage.postgres.entities.SemrackDocumentEntity;
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.List;
import java.util.Optional;
@ApplicationScoped
public class SemdocJpaStorage implements SemdocStorage, PanacheRepository<SemrackDocumentEntity> {
@Inject
UIDGenerator generator;
@Inject
SemrackLookupService lookupService;
@Override
@WithSession
public Uni<SemrackDocument> get(String uid) {
return find("uid = ?1", uid)
.firstResult().map(d -> Optional.ofNullable(d).map(SemrackDocumentEntity::getDocument).orElse(null));
}
@Override
@WithSession
public Uni<List<SemrackDocument>> get(List<String> uids) {
return find("uid in ?1", uids).list()
.map(d -> d.stream().map(SemrackDocumentEntity::getDocument).toList());
}
@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();
// }
// }
}