diff --git a/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AccountEntity.java b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AccountEntity.java new file mode 100644 index 00000000..38eaaebc --- /dev/null +++ b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AccountEntity.java @@ -0,0 +1,31 @@ +package io.github.perplexhub.rsql.model.account; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Entity +public class AccountEntity { + + @Id + private String ident; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "account") + private List addressHistory = new ArrayList<>(); + + public AccountEntity(String ident) { + this.ident = ident; + } + + public AccountEntity() { + } + +} diff --git a/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AddressEntity.java b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AddressEntity.java new file mode 100644 index 00000000..6d105023 --- /dev/null +++ b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AddressEntity.java @@ -0,0 +1,24 @@ +package io.github.perplexhub.rsql.model.account; + +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.Setter; + + +@Setter +@Getter +@Embeddable +public class AddressEntity { + + private String name; + private String address; + + public AddressEntity(String name, String address) { + this.name = name; + this.address = address; + } + + public AddressEntity() { + } + +} diff --git a/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AddressHistoryEntity.java b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AddressHistoryEntity.java new file mode 100644 index 00000000..9a96ef1b --- /dev/null +++ b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/account/AddressHistoryEntity.java @@ -0,0 +1,59 @@ +package io.github.perplexhub.rsql.model.account; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Entity +public class AddressHistoryEntity { + + @Setter + @Getter + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(name = "id", nullable = false) + private Long id; + + @Setter + @Getter + @ManyToOne + @JoinColumn(name = "account_ident") + AccountEntity account; + + OffsetDateTime activeSince; + + @Embedded + @AttributeOverride(name = "name", column = @Column(name = "invoice_name")) + @AttributeOverride(name = "address", column = @Column(name = "invoice_address")) + AddressEntity invoiceAddress; + + @Embedded + @AttributeOverride(name = "name", column = @Column(name = "shipping_name")) + @AttributeOverride(name = "address", column = @Column(name = "shipping_address")) + AddressEntity shippingAddress; + + public AddressHistoryEntity() { + } + + public AddressHistoryEntity( + AccountEntity account, + OffsetDateTime activeSince, + AddressEntity invoiceAddress, + AddressEntity shippingAddress) { + this.account = account; + this.activeSince = activeSince; + this.invoiceAddress = invoiceAddress; + this.shippingAddress = shippingAddress; + } + +} diff --git a/rsql-common/src/test/java/io/github/perplexhub/rsql/repository/jpa/AccountRepository.java b/rsql-common/src/test/java/io/github/perplexhub/rsql/repository/jpa/AccountRepository.java new file mode 100644 index 00000000..5641544f --- /dev/null +++ b/rsql-common/src/test/java/io/github/perplexhub/rsql/repository/jpa/AccountRepository.java @@ -0,0 +1,9 @@ +package io.github.perplexhub.rsql.repository.jpa; + +import io.github.perplexhub.rsql.model.account.AccountEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + + +public interface AccountRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAContext.java b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAContext.java index fc1cb0f4..3032e9a6 100644 --- a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAContext.java +++ b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAContext.java @@ -3,12 +3,14 @@ import jakarta.persistence.criteria.Path; import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.ManagedType; import lombok.Value; @Value(staticConstructor = "of") class RSQLJPAContext { - private Path path; - private Attribute attribute; + Path path; + Attribute attribute; + ManagedType managedType; } diff --git a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java index 596d9632..37f706e1 100644 --- a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java +++ b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java @@ -89,6 +89,7 @@ private RSQLJPAContext findPropertyPathInternal(String propertyPath, Path startR RSQLJPAContext context = findPropertyPathInternal(mappedProperty, root, firstTry); root = context.getPath(); attribute = context.getAttribute(); + classMetadata = context.getManagedType(); } else { if (!hasPropertyName(mappedProperty, classMetadata)) { Optional mayBeJSonPath = PathUtils @@ -165,7 +166,7 @@ private RSQLJPAContext findPropertyPathInternal(String propertyPath, Path startR accessControl(type, attribute.getName()); } - return RSQLJPAContext.of(root, attribute); + return RSQLJPAContext.of(root, attribute, classMetadata); } private String getKeyJoin(Path root, String mappedProperty) { diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java index a9300f03..c3bc94cc 100644 --- a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java @@ -11,9 +11,15 @@ import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.*; import io.github.perplexhub.rsql.custom.CustomType; +import io.github.perplexhub.rsql.model.account.AccountEntity; +import io.github.perplexhub.rsql.model.account.AddressEntity; +import io.github.perplexhub.rsql.model.account.AddressHistoryEntity; +import io.github.perplexhub.rsql.repository.jpa.AccountRepository; import io.github.perplexhub.rsql.repository.jpa.custom.CustomTypeRepository; import io.github.perplexhub.rsql.custom.EntityWithCustomType; import jakarta.persistence.criteria.Expression; @@ -23,10 +29,8 @@ import io.github.perplexhub.rsql.model.Status; import org.assertj.core.api.Assertions; import org.assertj.core.groups.Tuple; -import org.junit.Ignore; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -64,6 +68,9 @@ class RSQLJPASupportTest { @Autowired private CustomTypeRepository customTypeRepository; + @Autowired + AccountRepository accountRepository; + @BeforeAll static void beforeAll() { RSQLCommonSupport.addConverter(CustomType.class, CustomType::new); @@ -1362,11 +1369,82 @@ void resetDBBeforeTest() { assertEquals(0, size); } + @Test + void testSearchForActiveSince() { // this is only a simple query test + // Given + AccountEntity account1 = new AccountEntity("account-ident-0"); + account1.setAddressHistory( + List.of( + new AddressHistoryEntity( + account1, + OffsetDateTime.of(2024, 7, 1, 0, 0, 0, 0, ZoneOffset.UTC), + new AddressEntity("Name 1", "some address 1"), + new AddressEntity("Name 1", "some other address 2")), + new AddressHistoryEntity( + account1, + OffsetDateTime.of(1900, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), + new AddressEntity("Name 1", "old address 1"), + new AddressEntity("Name 1", "old address 1")))); + accountRepository.save(account1); + // When + List result = accountRepository.findAll(RSQLJPASupport.rsql("addressHistory.activeSince=ge=2024-06-01T00:00:00Z")); + // Then + Assertions.assertThat(result).hasSize(1); + } + + @Test + void testSearchInListWhichContainEmbeddedClass() { + // Given + AccountEntity account1 = new AccountEntity("account-ident-1"); + account1.setAddressHistory( + List.of( + new AddressHistoryEntity( + account1, + OffsetDateTime.of(2024, 7, 1, 0, 0, 0, 0, ZoneOffset.UTC), + new AddressEntity("Name 1", "some address 1"), + new AddressEntity("Name 1", "some other address 2")), + new AddressHistoryEntity( + account1, + OffsetDateTime.of(1900, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), + new AddressEntity("Name 1", "old address 1"), + new AddressEntity("Name 1", "old address 1")))); + accountRepository.save(account1); + // When + List result = accountRepository.findAll(RSQLJPASupport.rsql("addressHistory.invoiceAddress.name=='Name 1'")); + // Then + Assertions.assertThat(result).hasSize(1); + } + + @Test + void testSearchWithReducedPathUsingRSQLMapping() { + // Given + RSQLCommonSupport.addMapping(AccountEntity.class, "invoiceAddress", "addressHistory.invoiceAddress"); + AccountEntity account1 = new AccountEntity("account-ident-2"); + account1.setAddressHistory( + List.of( + new AddressHistoryEntity( + account1, + OffsetDateTime.of(2024, 7, 1, 0, 0, 0, 0, ZoneOffset.UTC), + new AddressEntity("Name 1", "some address 1"), + new AddressEntity("Name 1", "some other address 2")), + new AddressHistoryEntity( + account1, + OffsetDateTime.of(1900, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), + new AddressEntity("Name 1", "old address 1"), + new AddressEntity("Name 1", "old address 1")))); + accountRepository.save(account1); + // When + List result = accountRepository.findAll(RSQLJPASupport.rsql("invoiceAddress.name=='Name 1'")); + // Then + Assertions.assertThat(result).hasSize(1); + } + @BeforeEach void setUp() { getPropertyWhitelist().clear(); getPropertyBlacklist().clear(); customTypeRepository.deleteAll(); + accountRepository.deleteAll(); } }