diff --git a/.jpb/jpb-settings.xml b/.jpb/jpb-settings.xml new file mode 100644 index 0000000..fb1f21c --- /dev/null +++ b/.jpb/jpb-settings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 3786e2d..d3f0d18 100644 --- a/README.md +++ b/README.md @@ -124,67 +124,48 @@ fun yourFunctionNameHere(@SearchSpec specs: Specification): ResponseE } ``` -1. Using the equal operator `:` -Request : `/cars?search=color:Red` -![equal operator example](./docs/images/equal-example.gif) - -2. Using the not equal operator `!` -Request : `/cars?search=color!Red` -![not equal operator example](./docs/images/not-equal-example.gif) - -3. Using the greater than operator `>` -Request : `/cars?search=creationyear>2017` -> Note: You can use the `>:` operator as well. - -![greater than operator example](./docs/images/greater-than-example.gif) - -4. Using the less than operator `<` -Request : `/cars?search=price<100000` -> Note: You can use the `<:` operator as well. - -![less than operator example](./docs/images/less-than-example.gif) - -5. Using the starts with operator `*` -*For the ends with operator, simply place `*` at the beginning of the word*. -*For the contains operator, simply place `*` at the beginning and the end of the word*. -Request : `/cars?search=brand:Aston*` -![starts with operator example](./docs/images/starts-with-example.gif) - -6. Using the `OR` operator -Request : `/cars?search=color:Red OR color:Blue` -![or operator example](./docs/images/or-example.gif) - -7. Using the `AND` operator -Request : `/cars?search=brand:Aston* AND price<300000` -![and operator example](./docs/images/and-example.gif) - -8. Checking if value is or not in a list -Request : `/cars?search=color IN ['Red', 'Blue']` -Request : `/cars?search=color NOT IN ['Red', 'Blue']` -*Note: Spaces inside the brackets are not necessary* -*Note: You will need to encode the value (e.g. encodeURI) as brackets are not valid url parts* - -9. Using the `IS EMPTY` and `IS NOT EMPTY` operators for collection fields -Request : `/users?search=cars IS EMPTY` - -10. Using parenthesis +## Operators + +| Operator | Description | Example | +|----------------|---------------------------------|----------------------------------------------------| +| `:` | Equal | `color:Red` | +| `!` | Not equal | `color!Red` | +| `>` | Greater than | `creationyear>2017` | +| `>:` | Greater than eq | `creationyear>2017` | +| `<` | Less than | `price<100000` | +| `<:` | Less than eq | `price<100000` | +| `*` | Starts with | `brand:*Martin` | +| `*` | Ends with | `brand:Aston*` | +| `*` | Contains | `brand:*Martin*` | +| `OR` | Logical OR | `color:Red OR color:Blue` | +| `AND` | Logical AND | `brand:Aston* AND price<300000` | +| `IN` | Value is in list | `color IN ['Red', 'Blue']` | +| `NOT IN` | Value is not in list | `color NOT IN ['Red', 'Blue']` | +| `IS EMPTY` | Collection field is empty | `cars IS EMPTY` | +| `IS NOT EMPTY` | Collection field is not empty | `cars IS NOT EMPTY` | +| `IS NULL` | Field is null | `brand IS NULL` | +| `IS NOT NULL` | Field is not null | `brand IS NOT NULL` | +| `()` | Parenthesis | `brand:Nissan OR (brand:Chevrolet AND color:Blue)` | +| `BETWEEN` | Value is between two values | `creationyear BETWEEN 2017 AND 2019` | +| `NOT BETWEEN` | Value is not between two values | `creationyear NOT BETWEEN 2017 AND 2019` | + + +## Examples + +1. Using parenthesis Request : `/cars?search=( brand:Nissan OR brand:Chevrolet ) AND color:Blue` *Note: Spaces inside the parenthesis are not necessary* ![parenthesis example](./docs/images/parenthesis-example.gif) - -11. Using space in nouns +2. Using space in nouns Request : `/cars?search=model:'Spacetourer Business Lounge'` ![space example](./docs/images/space-example.gif) - -12. Using special characters +3. Using special characters Request: `/cars?search=model:中华V7` ![special characters example](./docs/images/special-characters-example.gif) - -13. Using deep fields +4. Using deep fields Request : `/cars?search=options.transmission:Auto` ![deep field example](./docs/images/deep-field-example.gif) - -14. Complex example +5. Complex example Request : `/cars?search=creationyear:2018 AND price<300000 AND (color:Yellow OR color:Blue) AND options.transmission:Auto` ![complex example](./docs/images/complex-example.gif) 15. Using the BETWEEN operator diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index fba830f..4234fd6 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -24,6 +24,7 @@ criteria is_value : EMPTY + | NULL ; key @@ -57,6 +58,10 @@ BOOL | 'false' ; +NULL + : 'NULL' + ; + STRING : '"' DoubleStringCharacter* '"' | '\'' SingleStringCharacter* '\'' diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt index e2367c4..d5fbcd7 100644 --- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt @@ -37,8 +37,7 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB } else { SearchOperation.IS_NOT } - val value = ctx.is_value!!.text - return toSpec(key, op, value) + return toSpec(key, op, ctx.is_value().text) } override fun visitEqArrayCriteria(ctx: QueryParser.EqArrayCriteriaContext): Specification { diff --git a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt index e44c49f..aa941db 100644 --- a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt +++ b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt @@ -28,6 +28,8 @@ enum class SearchOperation { val AND_OPERATOR = "AND" val LEFT_PARANTHESIS = "(" val RIGHT_PARANTHESIS = ")" + val EMPTY = "EMPTY" + val NULL = "NULL" /** * Parse a string into an operation. diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt index b87c182..2d854be 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt @@ -1,9 +1,11 @@ package com.sipios.springsearch.strategies +import com.sipios.springsearch.SearchOperation import kotlin.reflect.KClass class BooleanStrategy : ParsingStrategy { override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return value?.toBoolean() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt index 468f84e..9004e93 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt @@ -4,9 +4,10 @@ import com.sipios.springsearch.SearchOperation import jakarta.persistence.criteria.CriteriaBuilder import jakarta.persistence.criteria.Path import jakarta.persistence.criteria.Predicate +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException class CollectionStrategy : ParsingStrategy { - override fun buildPredicate( builder: CriteriaBuilder, path: Path<*>, @@ -14,12 +15,15 @@ class CollectionStrategy : ParsingStrategy { ops: SearchOperation?, value: Any? ): Predicate? { - if (ops == SearchOperation.IS && value != null) { + if (ops == SearchOperation.IS && value == SearchOperation.EMPTY) { return builder.isEmpty(path[fieldName]) } - if (ops == SearchOperation.IS_NOT && value != null) { + if (ops == SearchOperation.IS_NOT && value == SearchOperation.EMPTY) { return builder.isNotEmpty(path[fieldName]) } - throw IllegalArgumentException("Unsupported operation $ops for collection field $fieldName, only IS and IS_NOT are supported") + throw ResponseStatusException(HttpStatus.BAD_REQUEST, + "Unsupported operation $ops $value for collection field $fieldName, " + + "only IS EMPTY and IS NOT EMPTY are supported" + ) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt index 61f5c69..b4b5597 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt @@ -29,6 +29,7 @@ class DateStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return standardDateFormat.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt index 7b1ccc0..85cbbe8 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt @@ -24,6 +24,7 @@ class DoubleStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return value?.toDouble() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt index a722fe2..dee1e03 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt @@ -25,6 +25,7 @@ class DurationStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return Duration.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt index 4e74ee5..451d31b 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt @@ -1,9 +1,11 @@ package com.sipios.springsearch.strategies +import com.sipios.springsearch.SearchOperation import kotlin.reflect.KClass class EnumStrategy : ParsingStrategy { override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt index ea1d1e6..85f292d 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt @@ -24,6 +24,7 @@ class FloatStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return value?.toFloat() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt index 66294cd..65af72e 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt @@ -25,6 +25,7 @@ class InstantStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return Instant.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt index 21f3863..ba6ee55 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt @@ -24,6 +24,7 @@ class IntStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return value?.toInt() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt index 2b435f6..c9c0b31 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt @@ -25,6 +25,7 @@ class LocalDateStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return LocalDate.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt index 763dae5..00ef88b 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt @@ -25,6 +25,7 @@ class LocalDateTimeStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return LocalDateTime.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt index 7682689..cc8b34b 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt @@ -25,6 +25,7 @@ class LocalTimeStrategy : ParsingStrategy { } override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return LocalTime.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt index 2986bf8..b2670c3 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt @@ -14,7 +14,8 @@ import java.util.Date import java.util.UUID import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf - +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException interface ParsingStrategy { /** * Method to parse the value specified to the corresponding strategy @@ -59,6 +60,27 @@ interface ParsingStrategy { builder.not(inClause) } + SearchOperation.IS -> { + if (value == SearchOperation.NULL) { + builder.isNull(path.get(fieldName)) + } else { + // we should not call parent method for collection fields + // so this makes no sense to search for EMPTY with a non-collection field + throw ResponseStatusException(HttpStatus.BAD_REQUEST, + "Unsupported operation $ops $value for field $fieldName") + } + } + SearchOperation.IS_NOT -> { + if (value == SearchOperation.NULL) { + builder.isNotNull(path.get(fieldName)) + } else { + // we should not call parent method for collection fields + // so this makes no sense to search for NOT EMPTY with a non-collection field + throw ResponseStatusException(HttpStatus.BAD_REQUEST, + "Unsupported operation $ops $value for field $fieldName") + } + } + SearchOperation.EQUALS -> builder.equal(path.get(fieldName), value) SearchOperation.NOT_EQUALS -> builder.notEqual(path.get(fieldName), value) SearchOperation.STARTS_WITH -> builder.like(path[fieldName], "$value%") diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt index 22542f2..013eff7 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt @@ -1,10 +1,12 @@ package com.sipios.springsearch.strategies +import com.sipios.springsearch.SearchOperation import java.util.UUID import kotlin.reflect.KClass class UUIDStrategy : ParsingStrategy { override fun parse(value: String?, fieldClass: KClass): Any? { + if (value == SearchOperation.NULL) return value return UUID.fromString(value) } } diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt index f7f43ac..632e180 100644 --- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt +++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt @@ -7,6 +7,7 @@ import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.util.Date import java.util.UUID import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -1283,6 +1284,31 @@ class SpringSearchApplicationTest { Assertions.assertTrue(users.isEmpty()) } + @Test + fun cantSearchForEmptyWithNonFieldProperties() { + val johnBook = Book() + val john = Author() + john.name = "john" + john.addBook(johnBook) + authorRepository.save(john) + val janeBook = Book() + val jane = Author() + jane.name = "jane" + jane.addBook(janeBook) + authorRepository.save(jane) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("name IS EMPTY").build() + Assertions.assertThrows( + ResponseStatusException::class.java + ) { authorRepository.findAll(specification) } + val specification2 = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("name IS NOT EMPTY").build() + Assertions.assertThrows( + ResponseStatusException::class.java + ) { authorRepository.findAll(specification2) } + } @Test fun canGetAuthorsWithEmptyBookWithResult() { val johnBook = Book() @@ -1352,6 +1378,27 @@ class SpringSearchApplicationTest { val users = authorRepository.findAll(specification) Assertions.assertTrue(users.size == 0) } + @Test + fun cantGetAuthorsWithBooksNull() { + val john = Author() + john.name = "john" + authorRepository.save(john) + val jane = Author() + jane.name = "jane" + authorRepository.save(jane) + val spec = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("books IS NULL").build() + Assertions.assertThrows( + ResponseStatusException::class.java + ) { authorRepository.findAll(spec) } + val specNotNull = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("books IS NOT NULL").build() + Assertions.assertThrows( + ResponseStatusException::class.java + ) { authorRepository.findAll(specNotNull) } + } @Test fun canGetUsersWithNumberOfChildrenBetween() { @@ -1519,4 +1566,145 @@ class SpringSearchApplicationTest { ).withSearch("userId IN [").build() } } + + @Test + fun canGetUsersWithNullColumn() { + userRepository.save(Users(userFirstName = "john", type = null)) + userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)) + userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) + userRepository.save(Users(userFirstName = "jean", type = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("type IS NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("john", "jean"), setNames) + } + + @Test + fun canGetUsersWithNotNullColumn() { + userRepository.save(Users(userFirstName = "john", type = null)) + userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)) + userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) + userRepository.save(Users(userFirstName = "jean", type = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("type IS NOT NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("jane", "joe"), setNames) + } + + @Test + fun canGetUsersWithNotNullFirstName() { + userRepository.save(Users(userFirstName = "john", type = null)) + userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)) + userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) + userRepository.save(Users(userFirstName = "jean", type = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userFirstName IS NOT NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(4, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("john", "jane", "joe", "jean"), setNames) + } + + @Test + fun canGetUserWithNullSalary() { + userRepository.save(Users(userFirstName = "john", userSalary = 100.0F)) + userRepository.save(Users(userFirstName = "jane", userSalary = 1000.0F)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userSalary IS NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(0, users.size) + } + + @Test + fun canGetUsersWithUUIDNull() { + val userUUID = UUID.randomUUID() + userRepository.save(Users(userFirstName = "Diego", uuid = userUUID)) + userRepository.save(Users(userFirstName = "Diego two", uuid = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("uuid IS NULL").build() + val robotUsers = userRepository.findAll(specification) + Assertions.assertEquals(1, robotUsers.size) + Assertions.assertEquals(null, robotUsers[0].uuid) + } + + @Test + fun canGetUsersWithUpdatedDateAtNull() { + userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) + userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-11"))) + userRepository.save(Users(userFirstName = "joe", updatedDateAt = null)) + userRepository.save(Users(userFirstName = "jean", updatedDateAt = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedDateAt IS NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("joe", "jean"), setNames) + } + + @Test + fun canGetUsersWithUpdatedDateTimeAtNotNull() { + userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) + userRepository.save(Users(userFirstName = "jane", updatedAt = LocalDateTime.parse("2020-01-11T10:15:30"))) + userRepository.save(Users(userFirstName = "joe", updatedAt = null)) + userRepository.save(Users(userFirstName = "jean", updatedAt = LocalDateTime.parse("2020-01-13T10:15:30"))) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedAt IS NOT NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(3, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("john", "jane", "jean"), setNames) + } + @Test + fun canGetUsersWithActiveNull() { + userRepository.save(Users(userFirstName = "john", active = true)) + userRepository.save(Users(userFirstName = "jane", active = false)) + userRepository.save(Users(userFirstName = "joe", active = null)) + userRepository.save(Users(userFirstName = "jean", active = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("active IS NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("joe", "jean"), setNames) + } + @Test + fun canGetUsersWithUserChildrenNumberNull() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = null)) + userRepository.save(Users(userFirstName = "jean", userChildrenNumber = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userChildrenNumber IS NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("joe", "jean"), setNames) + } + @Test + fun canGetUsersWithCreatedAtNull() { + userRepository.save(Users(userFirstName = "john", createdAt = Date())) + userRepository.save(Users(userFirstName = "jane", createdAt = Date())) + userRepository.save(Users(userFirstName = "joe", createdAt = null)) + userRepository.save(Users(userFirstName = "jean", createdAt = null)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("createdAt IS NULL").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + val setNames = users.map { user -> user.userFirstName }.toSet() + Assertions.assertEquals(setOf("joe", "jean"), setNames) + } } diff --git a/src/test/kotlin/com/sipios/springsearch/Users.kt b/src/test/kotlin/com/sipios/springsearch/Users.kt index 24a47cc..8b91107 100644 --- a/src/test/kotlin/com/sipios/springsearch/Users.kt +++ b/src/test/kotlin/com/sipios/springsearch/Users.kt @@ -37,7 +37,7 @@ data class Users( var userAddress: String = "1 rue de l'angleterre", @Column(name = "NumberOfChildren") - var userChildrenNumber: Int = 3, + var userChildrenNumber: Int? = 3, @Column(name = "Salary") var userSalary: Float = 3000.0F, @@ -46,16 +46,16 @@ data class Users( var userAgeInSeconds: Double = 1261440000.0, @Column - var createdAt: Date = Date(), + var createdAt: Date? = Date(), @Column - var updatedAt: LocalDateTime = LocalDateTime.now(), + var updatedAt: LocalDateTime? = LocalDateTime.now(), @Column var updatedTimeAt: LocalTime = LocalTime.now(), @Column - var updatedDateAt: LocalDate = LocalDate.now(), + var updatedDateAt: LocalDate? = LocalDate.now(), @Column var updatedInstantAt: Instant = Instant.now(), @@ -67,5 +67,8 @@ data class Users( var type: UserType? = UserType.TEAM_MEMBER, @Column - var uuid: UUID = UUID.randomUUID() + var uuid: UUID? = UUID.randomUUID(), + + @Column + var active: Boolean? = true )