diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 2697c13..6ade699 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -10,14 +10,13 @@ jobs: build-lib: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v2 + - name: Set up JDK + uses: actions/setup-java@v4 with: - java-version: '8' - distribution: 'adopt' - cache: maven + java-version: '17' + distribution: 'temurin' - name: Build with Gradle run: ./gradlew build @@ -32,6 +31,7 @@ jobs: jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv - name: Commit and push the badge (if it changed) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: EndBug/add-and-commit@v7 with: default_author: github_actions @@ -44,30 +44,30 @@ jobs: name: jacoco-report path: target/site/jacoco/ - build-example-maven-java-8: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up JDK 8 - uses: actions/setup-java@v2 - with: - java-version: '8' - distribution: 'adopt' - cache: maven - - - name: Build Java Example Project with Maven - working-directory: ./examples/java-maven-h2 - run: mvn -B package --file pom.xml +# build-example-maven-java: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v3 +# +# - name: Set up JDK +# uses: actions/setup-java@v4 +# with: +# java-version: '17' +# distribution: 'temurin' +# cache: maven +# +# - name: Build Java Example Project with Maven +# working-directory: ./examples/java-maven-h2 +# run: mvn -B package --file pom.xml - build-example-gradle-java-17: + build-example-gradle-java: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: Set up JDK + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' diff --git a/.github/workflows/publish-package-pipeline.yml b/.github/workflows/publish-package-pipeline.yml index fb09d2a..f2d58b5 100644 --- a/.github/workflows/publish-package-pipeline.yml +++ b/.github/workflows/publish-package-pipeline.yml @@ -13,13 +13,13 @@ jobs: packages: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v2 + - name: Set up JDK + uses: actions/setup-java@v4 with: - java-version: '8' - distribution: 'adopt' + java-version: '17' + distribution: 'temurin' - name: Build with Gradle run: ./gradlew build diff --git a/README.md b/README.md index edab7b5..61a9d29 100644 --- a/README.md +++ b/README.md @@ -335,8 +335,9 @@ If you'd like to contribute code to this project you can do so through GitHub by By contributing your code, you agree to license your contribution under the terms of the Apache Licence. -## TODO -1. √ add reactive example -2. add documentation for between -3. remove the SEARCH_IN_SEPARATOR_PRM & SEARCH_IN_SEPARATOR_DEF -4. remove POSTGRES unaccent for reactive +# Spring Compatibility + +| jpa-magic-filter version | Spring Version | +|--------------------------|----------------| +| 1.0.* | 2.7.* | +| 3.2.* | 3.2.* | diff --git a/build.gradle.kts b/build.gradle.kts index c1278d4..167031b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,28 +2,26 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.util.Base64 plugins { - id("org.springframework.boot") version "2.7.3" - id("io.spring.dependency-management") version "1.0.13.RELEASE" - kotlin("jvm") version "1.6.21" - kotlin("plugin.spring") version "1.6.21" - kotlin("plugin.jpa") version "1.6.21" + id("org.springframework.boot") version "3.2.1" + id("io.spring.dependency-management") version "1.1.4" + kotlin("jvm") version "1.9.21" + kotlin("plugin.spring") version "1.9.21" + kotlin("plugin.jpa") version "1.9.21" - kotlin("kapt") version "1.6.21" - - id("org.jlleitschuh.gradle.ktlint") version "10.3.0" + id("org.jlleitschuh.gradle.ktlint") version "12.0.3" `java-library` `maven-publish` - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" + id("io.github.gradle-nexus.publish-plugin") version "1.3.0" signing jacoco } group = "io.github.verissimor.lib" -version = System.getenv("RELEASE_VERSION") ?: "1.0.8-SNAPSHOT" +version = System.getenv("RELEASE_VERSION") ?: "3.2.1a-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 withSourcesJar() withJavadocJar() } @@ -50,8 +48,6 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - kapt("org.springframework.boot:spring-boot-configuration-processor") - testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") @@ -59,13 +55,13 @@ dependencies { testImplementation("com.h2database:h2") testImplementation("org.springframework.boot:spring-boot-starter-data-jpa") testImplementation("org.springframework.boot:spring-boot-starter-web") - testImplementation("org.springframework.data:spring-data-relational:2.4.2") + testImplementation("org.springframework.data:spring-data-relational") } tasks.withType { kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = "1.8" + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "17" } } diff --git a/examples/java-gradle-reactive/build.gradle b/examples/java-gradle-reactive/build.gradle index 80d94d4..f57f61a 100644 --- a/examples/java-gradle-reactive/build.gradle +++ b/examples/java-gradle-reactive/build.gradle @@ -1,12 +1,15 @@ plugins { - id 'org.springframework.boot' version '2.7.3' - id 'io.spring.dependency-management' version '1.0.13.RELEASE' id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' } group = 'io.github.verissimor.examples.reactive' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' + +java { + sourceCompatibility = '17' +} repositories { mavenCentral() diff --git a/examples/java-gradle-reactive/gradle/wrapper/gradle-wrapper.properties b/examples/java-gradle-reactive/gradle/wrapper/gradle-wrapper.properties index 8049c68..db9a6b8 100644 --- a/examples/java-gradle-reactive/gradle/wrapper/gradle-wrapper.properties +++ b/examples/java-gradle-reactive/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/java-maven-h2/pom.xml b/examples/java-maven-h2/pom.xml index e19cba1..c72d57a 100644 --- a/examples/java-maven-h2/pom.xml +++ b/examples/java-maven-h2/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.5.8 + 3.2.1 io.github.verissimor.examples @@ -14,13 +14,12 @@ java-maven-h2 Demo project for Spring Boot - 1.8 + 17 org.springframework.boot spring-boot-starter-data-jpa - 2.6.2 org.springframework.boot diff --git a/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/City.java b/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/City.java index 4db200b..46df9dc 100644 --- a/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/City.java +++ b/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/City.java @@ -1,15 +1,16 @@ package io.github.verissimor.examples.javamavenh2.entity; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.lang.Nullable; -import static javax.persistence.GenerationType.IDENTITY; +import static jakarta.persistence.GenerationType.IDENTITY; + @Entity @Data @@ -18,7 +19,7 @@ public class City { @Id @GeneratedValue(strategy = IDENTITY) - @Nullable + @Nullable Long id; String name; } diff --git a/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/User.java b/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/User.java index 52c0517..de3da97 100644 --- a/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/User.java +++ b/examples/java-maven-h2/src/main/java/io/github/verissimor/examples/javamavenh2/entity/User.java @@ -1,24 +1,26 @@ package io.github.verissimor.examples.javamavenh2.entity; import java.time.LocalDate; -import javax.persistence.Entity; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.lang.Nullable; -import static javax.persistence.EnumType.STRING; -import static javax.persistence.GenerationType.IDENTITY; +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.GenerationType.IDENTITY; @Entity @Data @NoArgsConstructor @AllArgsConstructor +@Table(name = "users") public class User { @Id @GeneratedValue(strategy = IDENTITY) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180..7f93135 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102..ac72c34 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..0adc8e1 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/kotlin/io/github/verissimor/lib/fieldparser/FieldParser.kt b/src/main/kotlin/io/github/verissimor/lib/fieldparser/FieldParser.kt index 2d5cc8c..678ad8d 100644 --- a/src/main/kotlin/io/github/verissimor/lib/fieldparser/FieldParser.kt +++ b/src/main/kotlin/io/github/verissimor/lib/fieldparser/FieldParser.kt @@ -18,10 +18,12 @@ import org.slf4j.LoggerFactory import java.lang.reflect.Field object FieldParser { - private val log: Logger = LoggerFactory.getLogger(this::class.java) - fun parseFields(params: Map?>, clazz: Class<*>): List { + fun parseFields( + params: Map?>, + clazz: Class<*>, + ): List { return params.mapNotNull { (field, value) -> val parsedField = parseField(field, value, clazz) @@ -41,7 +43,11 @@ object FieldParser { } } - private fun parseField(field: String, value: List?, clazz: Class<*>): ParsedField { + private fun parseField( + field: String, + value: List?, + clazz: Class<*>, + ): ParsedField { val group: Int = parseGroup(field) val combineOperator: CombineOperator = if (field.startsWith("or__")) OR else AND val normalized = normalize(field, group) @@ -55,15 +61,19 @@ object FieldParser { return ParsedField(resolvedOperator, resolvedFieldName, fieldClass, value, group, combineOperator) } - private fun normalize(field: String, group: Int) = field.trim() + private fun normalize( + field: String, + group: Int, + ) = field.trim() .replace("[]", "") // remove array format of a few js libraries .replace("__$group", "") // remove the group .let { if (it.startsWith("or__")) it.replace("or__", "") else it } // remove the or private fun fieldToFilterOperator(field: String): FilterOperator { - val type = FilterOperator.values() - .sortedByDescending { it.suffix.length } - .firstOrNull { field.lowercase().endsWith(it.suffix) } ?: FilterOperator.EQUAL + val type = + FilterOperator.values() + .sortedByDescending { it.suffix.length } + .firstOrNull { field.lowercase().endsWith(it.suffix) } ?: FilterOperator.EQUAL return type } @@ -71,7 +81,7 @@ object FieldParser { private fun overloadFilterOperator( filterOperator: FilterOperator, value: List?, - fieldClass: Field? + fieldClass: Field?, ): FilterOperator { val shouldTryOverload = value != null && filterOperator == FilterOperator.EQUAL val fieldType: FieldType? = fieldClass.toFieldType() @@ -87,25 +97,32 @@ object FieldParser { val shouldTrySplitComma = value!!.size == 1 && listOf(ENUMERATED, NUMBER, LOCAL_DATE, UUID).contains(fieldType) if (shouldTryOverload && shouldTrySplitComma && value.firstOrNull()!!.contains(",")) { val list = parseStringIntoList(value.firstOrNull()!!) - if (list?.isNotEmpty() == true) + if (list?.isNotEmpty() == true) { return FilterOperator.IN + } } return filterOperator } - private fun resolveFieldName(field: String, type: FilterOperator?) = - type?.let { field.replace(it.suffix, "") } ?: field + private fun resolveFieldName( + field: String, + type: FilterOperator?, + ) = type?.let { field.replace(it.suffix, "") } ?: field - private fun fieldToClass(field: String, root: Class<*>): Field? { + private fun fieldToClass( + field: String, + root: Class<*>, + ): Field? { var resultField: Field? = null field.split(".") .forEach { fieldP -> - resultField = if (resultField == null) { - root.declaredFields.firstOrNull { it.name == fieldP } - } else { - resultField?.type?.declaredFields?.firstOrNull { it.name == fieldP } - } + resultField = + if (resultField == null) { + root.declaredFields.firstOrNull { it.name == fieldP } + } else { + resultField?.type?.declaredFields?.firstOrNull { it.name == fieldP } + } } return resultField diff --git a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/CombineOperator.kt b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/CombineOperator.kt index 1e72963..d97513d 100644 --- a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/CombineOperator.kt +++ b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/CombineOperator.kt @@ -1,5 +1,6 @@ package io.github.verissimor.lib.fieldparser.domain enum class CombineOperator { - AND, OR; + AND, + OR, } diff --git a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FieldType.kt b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FieldType.kt index 51ba88c..64c49a5 100644 --- a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FieldType.kt +++ b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FieldType.kt @@ -17,34 +17,36 @@ enum class FieldType { INSTANT, BOOLEAN, UUID, - GENERIC; + GENERIC, + ; companion object { val comparisonOperators = listOf(GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, BETWEEN) val comparisonTypes = listOf(NUMBER, LOCAL_DATE, INSTANT) - fun Field?.toFieldType(): FieldType? = when { - this == null -> null - this.type?.superclass?.name == "java.lang.Enum" -> ENUMERATED - this.type == LocalDate::class.java -> LOCAL_DATE - this.type == Instant::class.java -> INSTANT - this.type == Boolean::class.java || - // this solves conflicts between kotlin/java - this.type.name == "java.lang.Boolean" -> BOOLEAN + fun Field?.toFieldType(): FieldType? = + when { + this == null -> null + this.type?.superclass?.name == "java.lang.Enum" -> ENUMERATED + this.type == LocalDate::class.java -> LOCAL_DATE + this.type == Instant::class.java -> INSTANT + this.type == Boolean::class.java || + // this solves conflicts between kotlin/java + this.type.name == "java.lang.Boolean" -> BOOLEAN - this.type == java.util.UUID::class.java -> UUID + this.type == java.util.UUID::class.java -> UUID - this.type == Int::class.java || - this.type == Long::class.java || - this.type == BigDecimal::class.java || - this.type.isAssignableFrom(Number::class.java) || - // this solves conflicts between kotlin/java - this.type.name == "java.lang.Integer" || - this.type.name == "java.math.BigDecimal" || - this.type.name == "java.lang.Long" || - this.type.isAssignableFrom(java.lang.Number::class.java) -> NUMBER + this.type == Int::class.java || + this.type == Long::class.java || + this.type == BigDecimal::class.java || + this.type.isAssignableFrom(Number::class.java) || + // this solves conflicts between kotlin/java + this.type.name == "java.lang.Integer" || + this.type.name == "java.math.BigDecimal" || + this.type.name == "java.lang.Long" || + this.type.isAssignableFrom(java.lang.Number::class.java) -> NUMBER - else -> GENERIC - } + else -> GENERIC + } } } diff --git a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FilterOperator.kt b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FilterOperator.kt index 26de3b3..0185aef 100644 --- a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FilterOperator.kt +++ b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/FilterOperator.kt @@ -20,5 +20,5 @@ enum class FilterOperator(val suffix: String, val allowNullableValue: Boolean = BETWEEN("_is_between"), NOT_EQUAL("_ne"), - EQUAL("") + EQUAL(""), } diff --git a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ParsedField.kt b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ParsedField.kt index b7d7caa..69e588a 100644 --- a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ParsedField.kt +++ b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ParsedField.kt @@ -14,7 +14,6 @@ data class ParsedField( val group: Int, val combineOperator: CombineOperator, ) : ValueParser(resolvedFieldName, sourceValue) { - fun getFieldType(): FieldType? = fieldClass.toFieldType() fun validate() { diff --git a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ValueParser.kt b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ValueParser.kt index 5a9d0f0..8615f83 100644 --- a/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ValueParser.kt +++ b/src/main/kotlin/io/github/verissimor/lib/fieldparser/domain/ValueParser.kt @@ -8,9 +8,8 @@ import java.util.UUID abstract class ValueParser( private val resolvedFieldName: String, - private val sourceValue: List? + private val sourceValue: List?, ) { - fun getStringOrNull(): String? { val value = sourceValue?.firstOrNull() if (value?.isEmpty() == true) { @@ -20,11 +19,15 @@ abstract class ValueParser( } fun getString(): String = getStringOrNull()!! + fun getBigDecimalOrNull(): BigDecimal? = getStringOrNull()?.toBigDecimalOrNull() + fun getBigDecimal(): BigDecimal = getBigDecimalOrNull()!! + fun getLocalDateOrNull(): LocalDate? = getStringOrNull()?.toLocalDateOrNull() fun getLocalDate(): LocalDate = getLocalDateOrNull()!! + fun getInstantOrNull(): Instant? = getStringOrNull()?.toInstantOrNull() fun getInstant(): Instant = getInstantOrNull()!! @@ -32,59 +35,68 @@ abstract class ValueParser( fun getBooleanOrNull(): Boolean? = getStringOrNull()?.toBooleanOrNull() fun getBoolean(): Boolean = getBooleanOrNull()!! + fun getUUIDOrNull(): UUID? = getStringOrNull()?.toUUIDOrNull() fun getUUID(): UUID = getUUIDOrNull()!! - fun getListStringOrNull(): List? = when { - sourceValue == null -> null - sourceValue.size == 1 -> getStringOrNull()?.let { parseStringIntoList(it) } - sourceValue.size > 1 -> sourceValue.toList() - else -> error("field `$resolvedFieldName` has no value to filter by parseIn") - } + fun getListStringOrNull(): List? = + when { + sourceValue == null -> null + sourceValue.size == 1 -> getStringOrNull()?.let { parseStringIntoList(it) } + sourceValue.size > 1 -> sourceValue.toList() + else -> error("field `$resolvedFieldName` has no value to filter by parseIn") + } fun getListString(): List = getListStringOrNull()!! fun getListBigDecimal(): List = getListString().mapNotNull { it.toBigDecimalOrNull() } + fun getListLocalDate(): List = getListString().mapNotNull { it.toLocalDateOrNull() } + fun getListInstant(): List = getListString().mapNotNull { it.toInstantOrNull() } + fun getListBoolean(): List = getListString().mapNotNull { it.toBooleanOrNull() } + fun getListUUID(): List = getListString().mapNotNull { it.toUUIDOrNull() } companion object { - fun parseStringIntoList(str: String?): List? = - str?.split(",")?.toList()?.map { it.trim() }?.filter { it.isNotEmpty() } + fun parseStringIntoList(str: String?): List? = str?.split(",")?.toList()?.map { it.trim() }?.filter { it.isNotEmpty() } - fun String.toLocalDateOrNull(): LocalDate? = try { - LocalDate.parse(this) - } catch (ex: DateTimeParseException) { - null - } + fun String.toLocalDateOrNull(): LocalDate? = + try { + LocalDate.parse(this) + } catch (ex: DateTimeParseException) { + null + } - fun String.toInstantOrNull(): Instant? = try { - // Attempt to parse the input as an epoch timestamp - val timestamp = this.toLongOrNull() - if (timestamp != null) { - Instant.ofEpochSecond(timestamp) - } else { - // Attempt to parse the input as an ISO8601 date-time - Instant.parse(this) + fun String.toInstantOrNull(): Instant? = + try { + // Attempt to parse the input as an epoch timestamp + val timestamp = this.toLongOrNull() + if (timestamp != null) { + Instant.ofEpochSecond(timestamp) + } else { + // Attempt to parse the input as an ISO8601 date-time + Instant.parse(this) + } + } catch (e: DateTimeParseException) { + // Handle parsing errors (invalid format) + null } - } catch (e: DateTimeParseException) { - // Handle parsing errors (invalid format) - null - } - fun String.toBooleanOrNull(): Boolean? = when (this.trim().lowercase()) { - "true", "1" -> true - "false", "0" -> false - else -> null // Unable to parse the string as a boolean - } + fun String.toBooleanOrNull(): Boolean? = + when (this.trim().lowercase()) { + "true", "1" -> true + "false", "0" -> false + else -> null // Unable to parse the string as a boolean + } } - fun String.toUUIDOrNull(): UUID? = try { - UUID.fromString(this) - } catch (ex: IllegalArgumentException) { - null - } + fun String.toUUIDOrNull(): UUID? = + try { + UUID.fromString(this) + } catch (ex: IllegalArgumentException) { + null + } } diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/FieldParser.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/FieldParser.kt index 527452e..5f2c816 100644 --- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/FieldParser.kt +++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/FieldParser.kt @@ -2,12 +2,16 @@ package io.github.verissimor.lib.jpamagicfilter import io.github.verissimor.lib.fieldparser.domain.FilterOperator import io.github.verissimor.lib.jpamagicfilter.domain.ParsedField +import jakarta.persistence.criteria.Root import java.lang.reflect.Field -import javax.persistence.criteria.Root object FieldParser { - - fun parseField(field: String, value: Array?, clazz: Class<*>, root: Root): ParsedField { + fun parseField( + field: String, + value: Array?, + clazz: Class<*>, + root: Root, + ): ParsedField { val normalized = normalize(field) val filterType = fieldToType(normalized, value) val resolvedFieldName = resolveFieldName(normalized, filterType) @@ -18,10 +22,14 @@ object FieldParser { private fun normalize(field: String) = field.trim().replace("[]", "") - private fun fieldToType(field: String, value: Array?): FilterOperator { - val type = FilterOperator.values() - .sortedByDescending { it.suffix.length } - .firstOrNull { field.contains(it.suffix) } ?: FilterOperator.EQUAL + private fun fieldToType( + field: String, + value: Array?, + ): FilterOperator { + val type = + FilterOperator.values() + .sortedByDescending { it.suffix.length } + .firstOrNull { field.contains(it.suffix) } ?: FilterOperator.EQUAL if (value != null && type == FilterOperator.EQUAL && value.size > 1) { return FilterOperator.IN @@ -30,18 +38,24 @@ object FieldParser { return type } - private fun resolveFieldName(field: String, type: FilterOperator?) = - type?.let { field.replace(it.suffix, "") } ?: field + private fun resolveFieldName( + field: String, + type: FilterOperator?, + ) = type?.let { field.replace(it.suffix, "") } ?: field - private fun fieldToClass(field: String, root: Class<*>): Field? { + private fun fieldToClass( + field: String, + root: Class<*>, + ): Field? { var resultField: Field? = null field.split(".") .forEach { fieldP -> - resultField = if (resultField == null) { - root.declaredFields.firstOrNull { it.name == fieldP } - } else { - resultField?.type?.declaredFields?.firstOrNull { it.name == fieldP } - } + resultField = + if (resultField == null) { + root.declaredFields.firstOrNull { it.name == fieldP } + } else { + resultField?.type?.declaredFields?.firstOrNull { it.name == fieldP } + } } return resultField diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilter.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilter.kt index b36a9c7..e431620 100644 --- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilter.kt +++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilter.kt @@ -8,18 +8,21 @@ import java.time.Instant import java.time.LocalDate class MagicFilter( - private val parameterMap: Map> + private val parameterMap: Map>, ) { + fun toSpecification( + clazz: Class<*>, + dbFeatures: DbFeatures, + ): Specification = + Specification { root, _, cb -> + val parsed = PredicateParser.parsePredicates(parameterMap, clazz, root, cb, dbFeatures) - fun toSpecification(clazz: Class<*>, dbFeatures: DbFeatures): Specification = Specification { root, _, cb -> - val parsed = PredicateParser.parsePredicates(parameterMap, clazz, root, cb, dbFeatures) - - when (parameterMap.toSingleParameter(SEARCH_TYPE_PRM)) { - SEARCH_TYPE_AND, null -> cb.and(*parsed.toTypedArray()) - SEARCH_TYPE_OR -> cb.or(*parsed.toTypedArray()) - else -> error("Invalid searchType. Only allowed: and, or") + when (parameterMap.toSingleParameter(SEARCH_TYPE_PRM)) { + SEARCH_TYPE_AND, null -> cb.and(*parsed.toTypedArray()) + SEARCH_TYPE_OR -> cb.or(*parsed.toTypedArray()) + else -> error("Invalid searchType. Only allowed: and, or") + } } - } fun toSpecification(clazz: Class<*>): Specification = toSpecification(clazz, NONE) @@ -33,8 +36,13 @@ class MagicFilter( } fun Map?>.toSingleParameter(key: String): Any? = this[key]?.firstOrNull() + fun Array?.toSingleBigDecimal(): BigDecimal? = this?.firstOrNull()?.toString()?.toBigDecimal() + fun Array?.toSingleString(): String? = this?.firstOrNull()?.toString() + fun Array?.toSingleDate(): LocalDate? = this?.firstOrNull()?.toString()?.let { LocalDate.parse(it) } + fun Array?.toSingleInstant(): Instant? = this?.firstOrNull()?.toString()?.let { Instant.parse(it) } + fun Array?.toSingleBoolean(): Boolean? = this?.firstOrNull()?.toString()?.toBoolean() diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilterConfigurer.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilterConfigurer.kt index 06ab1a1..70529f8 100644 --- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilterConfigurer.kt +++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/MagicFilterConfigurer.kt @@ -22,7 +22,12 @@ class MagicFilterAttributeResolver : HandlerMethodArgumentResolver { return parameter.parameterType == MagicFilter::class.java } - override fun resolveArgument(parameter: MethodParameter, mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory?): Any { + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any { return MagicFilter(webRequest.parameterMap) } } diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/PredicateParser.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/PredicateParser.kt index f1a11b7..ccfee86 100644 --- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/PredicateParser.kt +++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/PredicateParser.kt @@ -28,17 +28,16 @@ import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.NONE import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.POSTGRES import io.github.verissimor.lib.jpamagicfilter.domain.ParsedField +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.Predicate +import jakarta.persistence.criteria.Root import org.slf4j.Logger import org.slf4j.LoggerFactory import java.math.BigDecimal import java.time.Instant import java.time.LocalDate -import javax.persistence.criteria.CriteriaBuilder -import javax.persistence.criteria.Predicate -import javax.persistence.criteria.Root object PredicateParser { - private val log: Logger = LoggerFactory.getLogger(this::class.java) fun parsePredicates( @@ -47,59 +46,84 @@ object PredicateParser { root: Root, cb: CriteriaBuilder, dbFeatures: DbFeatures, - ): List = params.mapNotNull { (field, value) -> - val parsedField = FieldParser.parseField(field, value, clazz, root) + ): List = + params.mapNotNull { (field, value) -> + val parsedField = FieldParser.parseField(field, value, clazz, root) - if (parsedField.fieldClass == null) { - log.info("Ignoring parameter $field") - return@mapNotNull null - } + if (parsedField.fieldClass == null) { + log.info("Ignoring parameter $field") + return@mapNotNull null + } - when (parsedField.filterOperator) { - EQUAL -> parseEqual(parsedField, value, cb) - NOT_EQUAL -> parseEqual(parsedField, value, cb).not() + when (parsedField.filterOperator) { + EQUAL -> parseEqual(parsedField, value, cb) + NOT_EQUAL -> parseEqual(parsedField, value, cb).not() - GREATER_THAN -> parseGreaterThan(parsedField, value, cb) - GREATER_THAN_EQUAL -> parseGreaterThanEqual(parsedField, value, cb) - LESS_THAN -> parseLessThan(parsedField, value, cb) - LESS_THAN_EQUAL -> parseLessThanEqual(parsedField, value, cb) + GREATER_THAN -> parseGreaterThan(parsedField, value, cb) + GREATER_THAN_EQUAL -> parseGreaterThanEqual(parsedField, value, cb) + LESS_THAN -> parseLessThan(parsedField, value, cb) + LESS_THAN_EQUAL -> parseLessThanEqual(parsedField, value, cb) - LIKE -> parseLike(parsedField, value, cb, dbFeatures) - LIKE_EXP -> parseLikeExp(parsedField, value, cb, dbFeatures) - NOT_LIKE -> parseNotLike(parsedField, value, cb, dbFeatures) - NOT_LIKE_EXP -> parseNotLikeExp(parsedField, value, cb, dbFeatures) + LIKE -> parseLike(parsedField, value, cb, dbFeatures) + LIKE_EXP -> parseLikeExp(parsedField, value, cb, dbFeatures) + NOT_LIKE -> parseNotLike(parsedField, value, cb, dbFeatures) + NOT_LIKE_EXP -> parseNotLikeExp(parsedField, value, cb, dbFeatures) - IN -> parseIn(parsedField, value, params) - NOT_IN -> parseNotIn(parsedField, value, params) + IN -> parseIn(parsedField, value, params) + NOT_IN -> parseNotIn(parsedField, value, params) - IS_NULL -> cb.isNull(parsedField.getPath()) - IS_NOT_NULL -> cb.isNotNull(parsedField.getPath()) + IS_NULL -> cb.isNull(parsedField.getPath()) + IS_NOT_NULL -> cb.isNotNull(parsedField.getPath()) - BETWEEN -> parseBetween(parsedField, value, params, cb) + BETWEEN -> parseBetween(parsedField, value, params, cb) + } } - } - private fun parseLike(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder, dbFeatures: DbFeatures) = when (dbFeatures) { + private fun parseLike( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { POSTGRES -> cb.like(cb.function("unaccent", String::class.java, cb.lower(parsedField.getPath())), "%${value.toSingleString()?.lowercase()?.unaccent()}%") NONE -> cb.like(cb.lower(parsedField.getPath()), "%${value.toSingleString()?.lowercase()}%") } - private fun parseLikeExp(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder, dbFeatures: DbFeatures) = when (dbFeatures) { + private fun parseLikeExp( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { POSTGRES -> cb.like(cb.function("unaccent", String::class.java, cb.lower(parsedField.getPath())), value.toSingleString()?.lowercase()?.unaccent()) NONE -> cb.like(cb.lower(parsedField.getPath()), value.toSingleString()?.lowercase()) } - private fun parseNotLike(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder, dbFeatures: DbFeatures) = when (dbFeatures) { + private fun parseNotLike( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { POSTGRES -> cb.notLike(cb.function("unaccent", String::class.java, cb.lower(parsedField.getPath())), "%${value.toSingleString()?.lowercase()?.unaccent()}%") NONE -> cb.notLike(cb.lower(parsedField.getPath()), "%${value.toSingleString()?.lowercase()}%") } - private fun parseNotLikeExp(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder, dbFeatures: DbFeatures) = when (dbFeatures) { + private fun parseNotLikeExp( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { POSTGRES -> cb.notLike(cb.function("unaccent", String::class.java, cb.lower(parsedField.getPath())), value.toSingleString()?.lowercase()?.unaccent()) NONE -> cb.notLike(cb.lower(parsedField.getPath()), value.toSingleString()?.lowercase()) } - private fun parseEqual(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder) = when (parsedField.getFieldType()) { + private fun parseEqual( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + ) = when (parsedField.getFieldType()) { ENUMERATED -> cb.equal(parsedField.getPath().`as`(String::class.java), value.toSingleString()) NUMBER -> cb.equal(parsedField.getPath(), value.toSingleBigDecimal()) LOCAL_DATE -> cb.equal(parsedField.getPath(), value.toSingleDate()) @@ -109,35 +133,55 @@ object PredicateParser { null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseEqual") } - private fun parseGreaterThan(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder) = when (parsedField.getFieldType()) { + private fun parseGreaterThan( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + ) = when (parsedField.getFieldType()) { NUMBER -> cb.gt(parsedField.getPath(), value.toSingleBigDecimal()) LOCAL_DATE -> cb.greaterThan(parsedField.getPath(), value.toSingleDate()) INSTANT -> cb.greaterThan(parsedField.getPath(), value.toSingleInstant()) ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThan") } - private fun parseGreaterThanEqual(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder) = when (parsedField.getFieldType()) { + private fun parseGreaterThanEqual( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + ) = when (parsedField.getFieldType()) { NUMBER -> cb.ge(parsedField.getPath(), value.toSingleBigDecimal()) LOCAL_DATE -> cb.greaterThanOrEqualTo(parsedField.getPath(), value.toSingleDate()) INSTANT -> cb.greaterThanOrEqualTo(parsedField.getPath(), value.toSingleInstant()) ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThanEqual") } - private fun parseLessThan(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder) = when (parsedField.getFieldType()) { + private fun parseLessThan( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + ) = when (parsedField.getFieldType()) { NUMBER -> cb.lt(parsedField.getPath(), value.toSingleBigDecimal()) LOCAL_DATE -> cb.lessThan(parsedField.getPath(), value.toSingleDate()) INSTANT -> cb.lessThan(parsedField.getPath(), value.toSingleInstant()) ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThan") } - private fun parseLessThanEqual(parsedField: ParsedField, value: Array?, cb: CriteriaBuilder) = when (parsedField.getFieldType()) { + private fun parseLessThanEqual( + parsedField: ParsedField, + value: Array?, + cb: CriteriaBuilder, + ) = when (parsedField.getFieldType()) { NUMBER -> cb.le(parsedField.getPath(), value.toSingleBigDecimal()) LOCAL_DATE -> cb.lessThanOrEqualTo(parsedField.getPath(), value.toSingleDate()) INSTANT -> cb.lessThanOrEqualTo(parsedField.getPath(), value.toSingleInstant()) ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThanEqual") } - private fun parseInValues(parsedField: ParsedField, value: Array?, params: Map?>): List { + private fun parseInValues( + parsedField: ParsedField, + value: Array?, + params: Map?>, + ): List { val separator: String = params.toSingleParameter(SEARCH_IN_SEPARATOR_PRM)?.toString() ?: SEARCH_IN_SEPARATOR_DEF return when { @@ -148,7 +192,11 @@ object PredicateParser { } } - private fun parseIn(parsedField: ParsedField, value: Array?, params: Map?>): Predicate? { + private fun parseIn( + parsedField: ParsedField, + value: Array?, + params: Map?>, + ): Predicate? { val values = parseInValues(parsedField, value, params) return when (parsedField.getFieldType()) { @@ -161,7 +209,11 @@ object PredicateParser { } } - private fun parseNotIn(parsedField: ParsedField, value: Array?, params: Map?>): Predicate? { + private fun parseNotIn( + parsedField: ParsedField, + value: Array?, + params: Map?>, + ): Predicate? { val values = parseInValues(parsedField, value, params) return when (parsedField.getFieldType()) { @@ -174,7 +226,12 @@ object PredicateParser { } } - private fun parseBetween(parsedField: ParsedField, value: Array?, params: Map?>, cb: CriteriaBuilder): Predicate? { + private fun parseBetween( + parsedField: ParsedField, + value: Array?, + params: Map?>, + cb: CriteriaBuilder, + ): Predicate? { val values = parseInValues(parsedField, value, params) return when (parsedField.getFieldType()) { diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/DbFeatures.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/DbFeatures.kt index e0ac49c..76b9f54 100644 --- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/DbFeatures.kt +++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/DbFeatures.kt @@ -2,5 +2,5 @@ package io.github.verissimor.lib.jpamagicfilter.domain enum class DbFeatures { NONE, - POSTGRES + POSTGRES, } diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt index 3526dcc..400deb6 100644 --- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt +++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt @@ -3,9 +3,9 @@ package io.github.verissimor.lib.jpamagicfilter.domain import io.github.verissimor.lib.fieldparser.domain.FieldType import io.github.verissimor.lib.fieldparser.domain.FieldType.Companion.toFieldType import io.github.verissimor.lib.fieldparser.domain.FilterOperator +import jakarta.persistence.criteria.Path +import jakarta.persistence.criteria.Root import java.lang.reflect.Field -import javax.persistence.criteria.Path -import javax.persistence.criteria.Root data class ParsedField( val root: Root, @@ -13,7 +13,6 @@ data class ParsedField( val resolvedFieldName: String, val fieldClass: Field?, ) { - fun getPath(): Path { var fullPath: Path? = null resolvedFieldName.split(".") diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/KotlinUtil.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/KotlinUtil.kt index b981bd6..192a66f 100644 --- a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/KotlinUtil.kt +++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/KotlinUtil.kt @@ -15,20 +15,35 @@ fun Map.toR2dbcMagicFilter(): R2dbcMagicFilter { fun magicFilterOfR2dbc(vararg prm: Pair): R2dbcMagicFilter = prm.toMap().toR2dbcMagicFilter() fun KProperty1.eq(value: Any): Pair = name to value.toString() + fun KProperty1.gt(value: Any): Pair = name + "_gt" to value.toString() + fun KProperty1.ge(value: Any): Pair = name + "_ge" to value.toString() + fun KProperty1.lt(value: Any): Pair = name + "_lt" to value.toString() + fun KProperty1.le(value: Any): Pair = name + "_le" to value.toString() + fun KProperty1.like(value: Any): Pair = name + "_like" to value.toString() + fun KProperty1.likeExp(value: Any): Pair = name + "_like_exp" to value.toString() + fun KProperty1.notLike(value: Any): Pair = name + "_not_like" to value.toString() + fun KProperty1.notLikeExp(value: Any): Pair = name + "_not_like_exp" to value.toString() + fun KProperty1.inValues(value: Collection): Pair = name + "_in" to value.joinToString(",") { it.toString() } + fun KProperty1.notInValues(value: Collection): Pair = name + "_not_in" to value.joinToString(",") { it.toString() } + fun KProperty1.isNull(): Pair = name + "_is_null" to "" + fun KProperty1.isNotNull(): Pair = name + "_is_not_null" to "" -fun DatabaseClient.sql(sql: String, binder: SqlBinder?): DatabaseClient.GenericExecuteSpec { +fun DatabaseClient.sql( + sql: String, + binder: SqlBinder?, +): DatabaseClient.GenericExecuteSpec { val startSql = this.sql(sql) if (binder == null) return startSql diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt index 5b54f9b..f26d2ee 100644 --- a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt +++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt @@ -19,60 +19,67 @@ import org.springframework.data.relational.core.query.Criteria import org.springframework.util.MultiValueMap class R2dbcMagicFilter( - parameterMap: MultiValueMap + parameterMap: MultiValueMap, ) { - - private val parameters: MutableMap?> = parameterMap.keys.associateWith { - parameterMap[it]?.toTypedArray()?.toList() - }.toMutableMap() + private val parameters: MutableMap?> = + parameterMap.keys.associateWith { + parameterMap[it]?.toTypedArray()?.toList() + }.toMutableMap() private val log: Logger = LoggerFactory.getLogger(R2dbcMagicFilter::class.java) - fun toCriteria(clazz: Class<*>, dbFeatures: DbFeatures = NONE): Criteria { - + fun toCriteria( + clazz: Class<*>, + dbFeatures: DbFeatures = NONE, + ): Criteria { val parseFields = toParsedFields(clazz) val groups = parseFields.map { it.group }.distinct().sorted() // first reduce/fold criteria in the group - val criteriaGroups = groups.map { group -> - val fieldsOfGroup = parseFields.filter { it.group == group } - - when (fieldsOfGroup.size) { - // just one field, reduces it as a single field - 1 -> R2dbcPredicateParser.parsePredicates(fieldsOfGroup.first(), dbFeatures) - - // many fields one record, reduces it as a collection of criteria - else -> fieldsOfGroup.fold(Criteria.empty()) { acc, field -> - val predicateOfField = R2dbcPredicateParser.parsePredicates(field, dbFeatures) - when (field.combineOperator) { - AND -> acc.and(predicateOfField) - OR -> acc.or(predicateOfField) - } + val criteriaGroups = + groups.map { group -> + val fieldsOfGroup = parseFields.filter { it.group == group } + + when (fieldsOfGroup.size) { + // just one field, reduces it as a single field + 1 -> R2dbcPredicateParser.parsePredicates(fieldsOfGroup.first(), dbFeatures) + + // many fields one record, reduces it as a collection of criteria + else -> + fieldsOfGroup.fold(Criteria.empty()) { acc, field -> + val predicateOfField = R2dbcPredicateParser.parsePredicates(field, dbFeatures) + when (field.combineOperator) { + AND -> acc.and(predicateOfField) + OR -> acc.or(predicateOfField) + } + } } } - } // then reduce the different groups - val criteria: Criteria = when (criteriaGroups.size) { - 1 -> criteriaGroups.first() - else -> criteriaGroups.fold(Criteria.empty()) { acc, criteria -> - when (getSearchType()) { - AND -> acc.and(criteria) - OR -> acc.or(criteria) - } + val criteria: Criteria = + when (criteriaGroups.size) { + 1 -> criteriaGroups.first() + else -> + criteriaGroups.fold(Criteria.empty()) { acc, criteria -> + when (getSearchType()) { + AND -> acc.and(criteria) + OR -> acc.or(criteria) + } + } } - } log.debug(criteria.toString()) return criteria } - fun getSearchType(): CombineOperator = when (parameters[SEARCH_TYPE_PRM]?.firstOrNull()) { - SEARCH_TYPE_AND, null -> AND - SEARCH_TYPE_OR -> OR - else -> error("Invalid searchType. Only allowed: and, or") - } + fun getSearchType(): CombineOperator = + when (parameters[SEARCH_TYPE_PRM]?.firstOrNull()) { + SEARCH_TYPE_AND, null -> AND + SEARCH_TYPE_OR -> OR + else -> error("Invalid searchType. Only allowed: and, or") + } fun toParsedFields(clazz: Class<*>): List { return FieldParser.parseFields(parameters, clazz) @@ -83,7 +90,7 @@ class R2dbcMagicFilter( operator: FilterOperator, value: List, combineOperator: CombineOperator = AND, - group: Int = 0 + group: Int = 0, ) { val combineOperatorStr = if (combineOperator == AND) "" else "or__" this.parameters[combineOperatorStr + fieldName + operator.suffix + "__$group"] = value.map { it.toString() } @@ -94,7 +101,7 @@ class R2dbcMagicFilter( operator: FilterOperator, value: Any, combineOperator: CombineOperator = AND, - group: Int = 0 + group: Int = 0, ) { addParameter(fieldName, operator, listOf(value.toString()), combineOperator, group) } @@ -103,14 +110,18 @@ class R2dbcMagicFilter( fieldName: String, operator: FilterOperator, combineOperator: CombineOperator = AND, - group: Int = 0 + group: Int = 0, ) { addParameter(fieldName, operator, emptyList(), combineOperator, group) } fun toCriteria(clazz: Class<*>): Criteria = toCriteria(clazz, NONE) - fun toSqlBinder(clazz: Class<*>, tableAlias: String? = null, startStr: String = " AND "): SqlBinder? { + fun toSqlBinder( + clazz: Class<*>, + tableAlias: String? = null, + startStr: String = " AND ", + ): SqlBinder? { val parseFields = toParsedFields(clazz) val groups = parseFields.map { it.group }.distinct().sorted() @@ -124,24 +135,27 @@ class R2dbcMagicFilter( } // first reduce/fold criteria in the group - val groupSqlList = groups.map { group -> - val fieldsOfGroup = parseFields.filter { it.group == group } - R2dbcSqlWriter.writeSql(fieldsOfGroup, tableAlias) - } + val groupSqlList = + groups.map { group -> + val fieldsOfGroup = parseFields.filter { it.group == group } + R2dbcSqlWriter.writeSql(fieldsOfGroup, tableAlias) + } val startBinder = SqlBinder("", emptyMap()) - val foldedBinder = groupSqlList.foldIndexed(startBinder) { idx, acc, sql -> - val newParams = acc.params + sql.params - val combineOperator = when { - idx == 0 -> "" - getSearchType() == AND -> "AND" - getSearchType() == OR -> "OR" - else -> error("Unexpected condition") - } + val foldedBinder = + groupSqlList.foldIndexed(startBinder) { idx, acc, sql -> + val newParams = acc.params + sql.params + val combineOperator = + when { + idx == 0 -> "" + getSearchType() == AND -> "AND" + getSearchType() == OR -> "OR" + else -> error("Unexpected condition") + } - val sqlStr = "${acc.sql} $combineOperator (${sql.sql})".trim() - SqlBinder(sqlStr, newParams) - } + val sqlStr = "${acc.sql} $combineOperator (${sql.sql})".trim() + SqlBinder(sqlStr, newParams) + } return SqlBinder("$startStr(" + foldedBinder.sql + ")", foldedBinder.params) } } diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt index 9917286..d5cd841 100644 --- a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt +++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt @@ -24,7 +24,11 @@ class R2dbcMagicFilterAttributeResolver : HandlerMethodArgumentResolver { return parameter.parameterType == R2dbcMagicFilter::class.java } - override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono { + override fun resolveArgument( + parameter: MethodParameter, + bindingContext: BindingContext, + exchange: ServerWebExchange, + ): Mono { return Mono.just(R2dbcMagicFilter(exchange.request.queryParams)) } } diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt index 520b2a6..7773180 100644 --- a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt +++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt @@ -33,109 +33,134 @@ import org.springframework.data.relational.core.query.Criteria import org.springframework.data.relational.core.query.Criteria.where object R2dbcPredicateParser { - private val log: Logger = LoggerFactory.getLogger(this::class.java) - fun parsePredicates(parsedField: ParsedField, dbFeatures: DbFeatures): Criteria = when (parsedField.filterOperator) { - EQUAL -> parseEqual(parsedField) - NOT_EQUAL -> parseNotEqual(parsedField) + fun parsePredicates( + parsedField: ParsedField, + dbFeatures: DbFeatures, + ): Criteria = + when (parsedField.filterOperator) { + EQUAL -> parseEqual(parsedField) + NOT_EQUAL -> parseNotEqual(parsedField) - GREATER_THAN -> parseGreaterThan(parsedField) - GREATER_THAN_EQUAL -> parseGreaterThanEqual(parsedField) - LESS_THAN -> parseLessThan(parsedField) - LESS_THAN_EQUAL -> parseLessThanEqual(parsedField) + GREATER_THAN -> parseGreaterThan(parsedField) + GREATER_THAN_EQUAL -> parseGreaterThanEqual(parsedField) + LESS_THAN -> parseLessThan(parsedField) + LESS_THAN_EQUAL -> parseLessThanEqual(parsedField) - LIKE -> parseLike(parsedField, dbFeatures).ignoreCase(true) - LIKE_EXP -> parseLikeExp(parsedField, dbFeatures).ignoreCase(true) - NOT_LIKE -> parseNotLike(parsedField, dbFeatures).ignoreCase(true) - NOT_LIKE_EXP -> parseNotLikeExp(parsedField, dbFeatures).ignoreCase(true) + LIKE -> parseLike(parsedField, dbFeatures).ignoreCase(true) + LIKE_EXP -> parseLikeExp(parsedField, dbFeatures).ignoreCase(true) + NOT_LIKE -> parseNotLike(parsedField, dbFeatures).ignoreCase(true) + NOT_LIKE_EXP -> parseNotLikeExp(parsedField, dbFeatures).ignoreCase(true) - IN -> parseIn(parsedField) - NOT_IN -> parseNotIn(parsedField) + IN -> parseIn(parsedField) + NOT_IN -> parseNotIn(parsedField) - IS_NULL -> where(parsedField.resolvedFieldName).isNull - IS_NOT_NULL -> where(parsedField.resolvedFieldName).isNotNull + IS_NULL -> where(parsedField.resolvedFieldName).isNull + IS_NOT_NULL -> where(parsedField.resolvedFieldName).isNotNull - BETWEEN -> parseBetween(parsedField) - } + BETWEEN -> parseBetween(parsedField) + } - private fun parseLike(parsedField: ParsedField, dbFeatures: DbFeatures) = when (dbFeatures) { - POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").like( - "%${parsedField.getString().lowercase().unaccent()}%" - ) + private fun parseLike( + parsedField: ParsedField, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { + POSTGRES -> + where("unaccent(lower(${parsedField.resolvedFieldName}))").like( + "%${parsedField.getString().lowercase().unaccent()}%", + ) NONE -> where(parsedField.resolvedFieldName).like("%${parsedField.getString().lowercase()}%") } - private fun parseLikeExp(parsedField: ParsedField, dbFeatures: DbFeatures) = when (dbFeatures) { - POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").like( - parsedField.getString().lowercase().unaccent() - ) + private fun parseLikeExp( + parsedField: ParsedField, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { + POSTGRES -> + where("unaccent(lower(${parsedField.resolvedFieldName}))").like( + parsedField.getString().lowercase().unaccent(), + ) NONE -> where(parsedField.resolvedFieldName).like(parsedField.getString().lowercase()) } - private fun parseNotLike(parsedField: ParsedField, dbFeatures: DbFeatures) = when (dbFeatures) { - POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").notLike( - "%${parsedField.getString().lowercase().unaccent()}%" - ) + private fun parseNotLike( + parsedField: ParsedField, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { + POSTGRES -> + where("unaccent(lower(${parsedField.resolvedFieldName}))").notLike( + "%${parsedField.getString().lowercase().unaccent()}%", + ) NONE -> where(parsedField.resolvedFieldName).notLike("%${parsedField.getString().lowercase()}%") } - private fun parseNotLikeExp(parsedField: ParsedField, dbFeatures: DbFeatures) = when (dbFeatures) { - POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").notLike( - parsedField.getString().lowercase().unaccent() - ) + private fun parseNotLikeExp( + parsedField: ParsedField, + dbFeatures: DbFeatures, + ) = when (dbFeatures) { + POSTGRES -> + where("unaccent(lower(${parsedField.resolvedFieldName}))").notLike( + parsedField.getString().lowercase().unaccent(), + ) NONE -> where(parsedField.resolvedFieldName).notLike(parsedField.getString().lowercase()) } - private fun parseEqual(parsedField: ParsedField) = when (parsedField.getFieldType()) { - ENUMERATED -> where(parsedField.resolvedFieldName).`is`(parsedField.getString()).ignoreCase(true) - NUMBER -> where(parsedField.resolvedFieldName).`is`(parsedField.getBigDecimal()) - LOCAL_DATE -> where(parsedField.resolvedFieldName).`is`(parsedField.getLocalDate()) - INSTANT -> where(parsedField.resolvedFieldName).`is`(parsedField.getInstant()) - BOOLEAN -> where(parsedField.resolvedFieldName).`is`(parsedField.getBoolean()) - UUID -> where(parsedField.resolvedFieldName).`is`(parsedField.getString()) - GENERIC -> where(parsedField.resolvedFieldName).`is`(parsedField.getString()).ignoreCase(true) - null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseEqual") - } + private fun parseEqual(parsedField: ParsedField) = + when (parsedField.getFieldType()) { + ENUMERATED -> where(parsedField.resolvedFieldName).`is`(parsedField.getString()).ignoreCase(true) + NUMBER -> where(parsedField.resolvedFieldName).`is`(parsedField.getBigDecimal()) + LOCAL_DATE -> where(parsedField.resolvedFieldName).`is`(parsedField.getLocalDate()) + INSTANT -> where(parsedField.resolvedFieldName).`is`(parsedField.getInstant()) + BOOLEAN -> where(parsedField.resolvedFieldName).`is`(parsedField.getBoolean()) + UUID -> where(parsedField.resolvedFieldName).`is`(parsedField.getString()) + GENERIC -> where(parsedField.resolvedFieldName).`is`(parsedField.getString()).ignoreCase(true) + null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseEqual") + } - private fun parseNotEqual(parsedField: ParsedField) = when (parsedField.getFieldType()) { - ENUMERATED -> where(parsedField.resolvedFieldName).not(parsedField.getString()).ignoreCase(true) - NUMBER -> where(parsedField.resolvedFieldName).not(parsedField.getBigDecimal()) - LOCAL_DATE -> where(parsedField.resolvedFieldName).not(parsedField.getLocalDate()) - INSTANT -> where(parsedField.resolvedFieldName).not(parsedField.getInstant()) - BOOLEAN -> where(parsedField.resolvedFieldName).not(parsedField.getBoolean()) - UUID -> where(parsedField.resolvedFieldName).not(parsedField.getString()) - GENERIC -> where(parsedField.resolvedFieldName).not(parsedField.getString()).ignoreCase(true) - null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseEqual") - } + private fun parseNotEqual(parsedField: ParsedField) = + when (parsedField.getFieldType()) { + ENUMERATED -> where(parsedField.resolvedFieldName).not(parsedField.getString()).ignoreCase(true) + NUMBER -> where(parsedField.resolvedFieldName).not(parsedField.getBigDecimal()) + LOCAL_DATE -> where(parsedField.resolvedFieldName).not(parsedField.getLocalDate()) + INSTANT -> where(parsedField.resolvedFieldName).not(parsedField.getInstant()) + BOOLEAN -> where(parsedField.resolvedFieldName).not(parsedField.getBoolean()) + UUID -> where(parsedField.resolvedFieldName).not(parsedField.getString()) + GENERIC -> where(parsedField.resolvedFieldName).not(parsedField.getString()).ignoreCase(true) + null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseEqual") + } - private fun parseGreaterThan(parsedField: ParsedField) = when (parsedField.getFieldType()) { - NUMBER -> where(parsedField.resolvedFieldName).greaterThan(parsedField.getBigDecimal()) - LOCAL_DATE -> where(parsedField.resolvedFieldName).greaterThan(parsedField.getLocalDate()) - INSTANT -> where(parsedField.resolvedFieldName).greaterThan(parsedField.getInstant()) - ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThan") - } + private fun parseGreaterThan(parsedField: ParsedField) = + when (parsedField.getFieldType()) { + NUMBER -> where(parsedField.resolvedFieldName).greaterThan(parsedField.getBigDecimal()) + LOCAL_DATE -> where(parsedField.resolvedFieldName).greaterThan(parsedField.getLocalDate()) + INSTANT -> where(parsedField.resolvedFieldName).greaterThan(parsedField.getInstant()) + ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThan") + } - private fun parseGreaterThanEqual(parsedField: ParsedField) = when (parsedField.getFieldType()) { - NUMBER -> where(parsedField.resolvedFieldName).greaterThanOrEquals(parsedField.getBigDecimal()) - LOCAL_DATE -> where(parsedField.resolvedFieldName).greaterThanOrEquals(parsedField.getLocalDate()) - INSTANT -> where(parsedField.resolvedFieldName).greaterThanOrEquals(parsedField.getInstant()) - ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThanEqual") - } + private fun parseGreaterThanEqual(parsedField: ParsedField) = + when (parsedField.getFieldType()) { + NUMBER -> where(parsedField.resolvedFieldName).greaterThanOrEquals(parsedField.getBigDecimal()) + LOCAL_DATE -> where(parsedField.resolvedFieldName).greaterThanOrEquals(parsedField.getLocalDate()) + INSTANT -> where(parsedField.resolvedFieldName).greaterThanOrEquals(parsedField.getInstant()) + ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThanEqual") + } - private fun parseLessThan(parsedField: ParsedField) = when (parsedField.getFieldType()) { - NUMBER -> where(parsedField.resolvedFieldName).lessThan(parsedField.getBigDecimal()) - LOCAL_DATE -> where(parsedField.resolvedFieldName).lessThan(parsedField.getLocalDate()) - INSTANT -> where(parsedField.resolvedFieldName).lessThan(parsedField.getInstant()) - ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThan") - } + private fun parseLessThan(parsedField: ParsedField) = + when (parsedField.getFieldType()) { + NUMBER -> where(parsedField.resolvedFieldName).lessThan(parsedField.getBigDecimal()) + LOCAL_DATE -> where(parsedField.resolvedFieldName).lessThan(parsedField.getLocalDate()) + INSTANT -> where(parsedField.resolvedFieldName).lessThan(parsedField.getInstant()) + ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThan") + } - private fun parseLessThanEqual(parsedField: ParsedField) = when (parsedField.getFieldType()) { - NUMBER -> where(parsedField.resolvedFieldName).lessThanOrEquals(parsedField.getBigDecimal()) - LOCAL_DATE -> where(parsedField.resolvedFieldName).lessThanOrEquals(parsedField.getLocalDate()) - INSTANT -> where(parsedField.resolvedFieldName).lessThanOrEquals(parsedField.getInstant()) - ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThanEqual") - } + private fun parseLessThanEqual(parsedField: ParsedField) = + when (parsedField.getFieldType()) { + NUMBER -> where(parsedField.resolvedFieldName).lessThanOrEquals(parsedField.getBigDecimal()) + LOCAL_DATE -> where(parsedField.resolvedFieldName).lessThanOrEquals(parsedField.getLocalDate()) + INSTANT -> where(parsedField.resolvedFieldName).lessThanOrEquals(parsedField.getInstant()) + ENUMERATED, GENERIC, BOOLEAN, UUID, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThanEqual") + } private fun parseIn(parsedField: ParsedField): Criteria { return when (parsedField.getFieldType()) { diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/R2dbcSqlWriter.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/R2dbcSqlWriter.kt index 7f6f224..4432652 100644 --- a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/R2dbcSqlWriter.kt +++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/R2dbcSqlWriter.kt @@ -25,123 +25,129 @@ import io.github.verissimor.lib.fieldparser.domain.FilterOperator.NOT_LIKE_EXP import io.github.verissimor.lib.fieldparser.domain.ParsedField object R2dbcSqlWriter { - - fun writeSql(parsedFields: List, tableAlias: String? = null): SqlBinder { - val alias = when { - tableAlias == null -> "" - tableAlias.endsWith(".") -> tableAlias - else -> "$tableAlias." - } + fun writeSql( + parsedFields: List, + tableAlias: String? = null, + ): SqlBinder { + val alias = + when { + tableAlias == null -> "" + tableAlias.endsWith(".") -> tableAlias + else -> "$tableAlias." + } val params = mutableMapOf() - val sqlWhere = parsedFields.mapIndexed { index: Int, field: ParsedField -> - val andOr = if (index > 0) field.combineOperator.toString() else "" - val fieldName = alias + camelToSnake(field.resolvedFieldName) - val paramIndex = (field.group * 100) + index - val fieldParam = field.resolvedFieldName + paramIndex + val sqlWhere = + parsedFields.mapIndexed { index: Int, field: ParsedField -> + val andOr = if (index > 0) field.combineOperator.toString() else "" + val fieldName = alias + camelToSnake(field.resolvedFieldName) + val paramIndex = (field.group * 100) + index + val fieldParam = field.resolvedFieldName + paramIndex - when (field.filterOperator) { - GREATER_THAN -> { - // AND b.name > :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName > :$fieldParam" - } - GREATER_THAN_EQUAL -> { - // AND b.name >= :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName >= :$fieldParam" - } - LESS_THAN -> { - // AND b.name < :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName < :$fieldParam" - } - LESS_THAN_EQUAL -> { - // AND b.name <= :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName <= :$fieldParam" - } - LIKE_EXP -> { - // AND b.name LIKE :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName LIKE :$fieldParam" - } - LIKE -> { - // AND b.name LIKE :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName LIKE :$fieldParam" - } - NOT_LIKE_EXP -> { - // AND b.name NOT LIKE :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName NOT LIKE :$fieldParam" - } - NOT_LIKE -> { - // AND b.name NOT LIKE :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName NOT LIKE :$fieldParam" - } - IN -> { - // AND b.name NOT IN (:name1) - params[fieldParam] = resolveListFieldValue(field) - "$andOr $fieldName IN (:$fieldParam)" + when (field.filterOperator) { + GREATER_THAN -> { + // AND b.name > :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName > :$fieldParam" + } + GREATER_THAN_EQUAL -> { + // AND b.name >= :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName >= :$fieldParam" + } + LESS_THAN -> { + // AND b.name < :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName < :$fieldParam" + } + LESS_THAN_EQUAL -> { + // AND b.name <= :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName <= :$fieldParam" + } + LIKE_EXP -> { + // AND b.name LIKE :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName LIKE :$fieldParam" + } + LIKE -> { + // AND b.name LIKE :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName LIKE :$fieldParam" + } + NOT_LIKE_EXP -> { + // AND b.name NOT LIKE :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName NOT LIKE :$fieldParam" + } + NOT_LIKE -> { + // AND b.name NOT LIKE :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName NOT LIKE :$fieldParam" + } + IN -> { + // AND b.name NOT IN (:name1) + params[fieldParam] = resolveListFieldValue(field) + "$andOr $fieldName IN (:$fieldParam)" + } + NOT_IN -> { + // AND b.name NOT IN (:name1) + params[fieldParam] = resolveListFieldValue(field) + "$andOr $fieldName NOT IN (:$fieldParam)" + } + IS_NULL -> { + // AND b.name IS NULL + "$andOr $fieldName IS NULL" + } + IS_NOT_NULL -> { + // AND b.name IS NULL + "$andOr $fieldName IS NOT NULL" + } + BETWEEN -> { + params["${fieldParam}a"] = resolveListFieldValue(field)[0] + params["${fieldParam}b"] = resolveListFieldValue(field)[1] + "$andOr $fieldName BETWEEN :${fieldParam}a AND :${fieldParam}b" + } + NOT_EQUAL -> { + // AND b.name <> :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName <> :$fieldParam" + } + EQUAL -> { + // AND b.name = :name1 + params[fieldParam] = resolveFieldValue(field) + "$andOr $fieldName = :$fieldParam" + } } - NOT_IN -> { - // AND b.name NOT IN (:name1) - params[fieldParam] = resolveListFieldValue(field) - "$andOr $fieldName NOT IN (:$fieldParam)" - } - IS_NULL -> { - // AND b.name IS NULL - "$andOr $fieldName IS NULL" - } - IS_NOT_NULL -> { - // AND b.name IS NULL - "$andOr $fieldName IS NOT NULL" - } - BETWEEN -> { - params["${fieldParam}a"] = resolveListFieldValue(field)[0] - params["${fieldParam}b"] = resolveListFieldValue(field)[1] - "$andOr $fieldName BETWEEN :${fieldParam}a AND :${fieldParam}b" - } - NOT_EQUAL -> { - // AND b.name <> :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName <> :$fieldParam" - } - EQUAL -> { - // AND b.name = :name1 - params[fieldParam] = resolveFieldValue(field) - "$andOr $fieldName = :$fieldParam" - } - } - }.joinToString(separator = " ") { it.trim() } + }.joinToString(separator = " ") { it.trim() } return SqlBinder(sqlWhere, params) } - private fun resolveFieldValue(parsed: ParsedField) = when (parsed.getFieldType()) { - ENUMERATED -> parsed.getString() - NUMBER -> parsed.getBigDecimal() - LOCAL_DATE -> parsed.getLocalDate() - INSTANT -> parsed.getInstant() - BOOLEAN -> parsed.getBoolean() - UUID -> parsed.getUUID() - GENERIC -> parsed.getString() - null -> error("Invalid fieldType") - } + private fun resolveFieldValue(parsed: ParsedField) = + when (parsed.getFieldType()) { + ENUMERATED -> parsed.getString() + NUMBER -> parsed.getBigDecimal() + LOCAL_DATE -> parsed.getLocalDate() + INSTANT -> parsed.getInstant() + BOOLEAN -> parsed.getBoolean() + UUID -> parsed.getUUID() + GENERIC -> parsed.getString() + null -> error("Invalid fieldType") + } - private fun resolveListFieldValue(parsed: ParsedField) = when (parsed.getFieldType()) { - ENUMERATED -> parsed.getListString() - NUMBER -> parsed.getListBigDecimal() - LOCAL_DATE -> parsed.getListLocalDate() - INSTANT -> parsed.getListInstant() - BOOLEAN -> parsed.getListBoolean() - UUID -> parsed.getListUUID() - GENERIC -> parsed.getListString() - null -> error("Invalid fieldType") - } + private fun resolveListFieldValue(parsed: ParsedField) = + when (parsed.getFieldType()) { + ENUMERATED -> parsed.getListString() + NUMBER -> parsed.getListBigDecimal() + LOCAL_DATE -> parsed.getListLocalDate() + INSTANT -> parsed.getListInstant() + BOOLEAN -> parsed.getListBoolean() + UUID -> parsed.getListUUID() + GENERIC -> parsed.getListString() + null -> error("Invalid fieldType") + } private fun camelToSnake(camelCase: String): String { val pattern = "(?<=[a-zA-Z])[A-Z]".toRegex() diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/SqlBinder.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/SqlBinder.kt index 04460aa..318ca37 100644 --- a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/SqlBinder.kt +++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/sqlwriter/SqlBinder.kt @@ -2,9 +2,8 @@ package io.github.verissimor.lib.r2dbcmagicfilter.sqlwriter data class SqlBinder( val sql: String, - val params: Map + val params: Map, ) { - operator fun plus(other: Any?): SqlBinder { if (other == null) return this diff --git a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserCombineOperatorTest.kt b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserCombineOperatorTest.kt index 543fa1e..b93f93b 100644 --- a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserCombineOperatorTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserCombineOperatorTest.kt @@ -8,7 +8,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class FieldParserCombineOperatorTest { - @Test fun `should parse combine operator`() { val params = listOf("name" to "Joe", "or__name" to "Jane").toMultiMap() diff --git a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserFieldTypeTest.kt b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserFieldTypeTest.kt index 96188a3..d82c5d9 100644 --- a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserFieldTypeTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserFieldTypeTest.kt @@ -16,7 +16,6 @@ import java.time.Instant import java.time.LocalDate class FieldParserFieldTypeTest { - @Test fun `should parse field type Generic String`() { val params = listOf("name" to "Joe").toMultiMap() diff --git a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserGroupTest.kt b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserGroupTest.kt index 93db4e1..7333a66 100644 --- a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserGroupTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserGroupTest.kt @@ -6,7 +6,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class FieldParserGroupTest { - @Test fun `should default group to zero`() { val params = listOf("name" to "Joe").toMultiMap() diff --git a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserOperatorTest.kt b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserOperatorTest.kt index 86cf98a..f62888d 100644 --- a/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserOperatorTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/fieldparser/FieldParserOperatorTest.kt @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class FieldParserOperatorTest { - @Test fun `should parse operator EQUAL`() { val params = listOf("name" to "Joe").toMultiMap() diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/BaseTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/BaseTest.kt index 4c4aa38..286d31f 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/BaseTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/BaseTest.kt @@ -10,7 +10,6 @@ import org.springframework.web.context.WebApplicationContext @AutoConfigureMockMvc @SpringBootTest(classes = [DemoApplication::class]) abstract class BaseTest { - @Autowired lateinit var objectMapper: ObjectMapper diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/CombineOperatorTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/CombineOperatorTest.kt index d7cf407..776419e 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/CombineOperatorTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/CombineOperatorTest.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class CombineOperatorTest : BaseTest() { - @Test fun `should filter like`() { mockMvc.get("/api/users?age=19&gender=FEMALE&searchType=and") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt index 73fa5cc..7fadc1e 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt @@ -7,6 +7,14 @@ import io.github.verissimor.lib.jpamagicfilter.Timezone.AMERICA_SAO_PAULO import io.github.verissimor.lib.jpamagicfilter.Timezone.EUROPE_LONDON import io.github.verissimor.lib.jpamagicfilter.Timezone.EUROPE_PARIS import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.POSTGRES +import jakarta.persistence.Entity +import jakarta.persistence.EnumType.STRING +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner import org.springframework.boot.autoconfigure.SpringBootApplication @@ -22,14 +30,6 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.time.Instant import java.time.LocalDate -import javax.persistence.Entity -import javax.persistence.EnumType.STRING -import javax.persistence.Enumerated -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.ManyToOne -import javax.persistence.Table import java.time.Instant.parse as instant import java.time.LocalDate.parse as date @@ -44,9 +44,8 @@ fun main(args: Array) { @RestController @RequestMapping("/api/users") class UserController( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) { - @GetMapping fun getCurrentUser(filter: MagicFilter): List { val specification: Specification = filter.toSpecification(User::class.java) @@ -70,7 +69,7 @@ data class User( val age: Int, @Enumerated(STRING) val gender: Gender, @ManyToOne val city: City, - val createdDate: LocalDate? + val createdDate: LocalDate?, ) @Entity @@ -81,11 +80,15 @@ data class City( val name: String, @ManyToOne val country: Country, @Enumerated(STRING) val timezone: Timezone, - val createdAt: Instant + val createdAt: Instant, ) @Entity -data class Country(@Id val code: String, val name: String, val isInEurope: Boolean) +data class Country( + @Id val code: String, + val name: String, + val isInEurope: Boolean, +) enum class Gender { FEMALE, MALE } @@ -94,7 +97,11 @@ enum class Timezone { AMERICA_NEW_YORK, EUROPE_LONDON, EUROPE_PARIS, AMERICA_SAO @Repository interface UserRepository : JpaRepository { fun findAll(spec: Specification?): List - fun findAll(spec: Specification?, pageable: Pageable?): Page + + fun findAll( + spec: Specification?, + pageable: Pageable?, + ): Page } @Repository @@ -110,34 +117,34 @@ class LoadData( private val countryRepository: CountryRepository, ) : ApplicationRunner { override fun run(args: ApplicationArguments?) { - val countries = listOf( - Country("US", "United States", false), - Country("UK", "United Kingdom", true), - Country("FR", "France", true), - Country("BR", "Brazil", false), - ) - - val cities = listOf( - City(null, "New York", countries[0], AMERICA_NEW_YORK, instant("2000-01-01T00:00:00.0Z")), - City(null, "London", countries[1], EUROPE_LONDON, instant("2000-01-01T00:00:00.0Z")), - City(null, "Paris", countries[2], EUROPE_PARIS, instant("2022-12-31T23:59:59.9Z")), - City(null, "Rio de Janeiro", countries[3], AMERICA_SAO_PAULO, instant("2022-12-31T23:59:59.9Z")), - ) - - val users = listOf( - User(null, "Matthew C. McAfee", 31, MALE, cities[0], date("2000-01-01")), - User(null, "Eleanor C. Moyer", 23, FEMALE, cities[0], date("2000-02-05")), - User(null, "Gloria D. Wells", 41, FEMALE, cities[0], date("2000-05-16")), - - User(null, "Matthew Norton", 43, MALE, cities[1], date("2000-03-12")), - User(null, "Maddison Joyce", 66, FEMALE, cities[1], date("2000-05-16")), - - User(null, "Xarles Foucault", 55, MALE, cities[2], date("2000-07-22")), - User(null, "Joy Rochefort", 19, FEMALE, cities[2], date("2000-08-10")), - - User(null, "Erick Melo Rodrigues", 19, MALE, cities[3], date("2000-10-30")), - User(null, "Gloria Azevedo Melot", 35, FEMALE, cities[3], null), - ) + val countries = + listOf( + Country("US", "United States", false), + Country("UK", "United Kingdom", true), + Country("FR", "France", true), + Country("BR", "Brazil", false), + ) + + val cities = + listOf( + City(null, "New York", countries[0], AMERICA_NEW_YORK, instant("2000-01-01T00:00:00.0Z")), + City(null, "London", countries[1], EUROPE_LONDON, instant("2000-01-01T00:00:00.0Z")), + City(null, "Paris", countries[2], EUROPE_PARIS, instant("2022-12-31T23:59:59.9Z")), + City(null, "Rio de Janeiro", countries[3], AMERICA_SAO_PAULO, instant("2022-12-31T23:59:59.9Z")), + ) + + val users = + listOf( + User(null, "Matthew C. McAfee", 31, MALE, cities[0], date("2000-01-01")), + User(null, "Eleanor C. Moyer", 23, FEMALE, cities[0], date("2000-02-05")), + User(null, "Gloria D. Wells", 41, FEMALE, cities[0], date("2000-05-16")), + User(null, "Matthew Norton", 43, MALE, cities[1], date("2000-03-12")), + User(null, "Maddison Joyce", 66, FEMALE, cities[1], date("2000-05-16")), + User(null, "Xarles Foucault", 55, MALE, cities[2], date("2000-07-22")), + User(null, "Joy Rochefort", 19, FEMALE, cities[2], date("2000-08-10")), + User(null, "Erick Melo Rodrigues", 19, MALE, cities[3], date("2000-10-30")), + User(null, "Gloria Azevedo Melot", 35, FEMALE, cities[3], null), + ) countryRepository.saveAll(countries) cityRepository.saveAll(cities) diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/NestedClassTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/NestedClassTest.kt index 931bd10..b0d10e8 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/NestedClassTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/NestedClassTest.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class NestedClassTest : BaseTest() { - @Test fun `should filter a nested class`() { mockMvc.get("/api/users?city.name=London") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/InstantFieldTypeTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/InstantFieldTypeTest.kt index f84c862..c0ee9a2 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/InstantFieldTypeTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/InstantFieldTypeTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class InstantFieldTypeTest : BaseTest() { - @Test fun `should filter equals a Instant`() { mockMvc.get("/api/users?city.createdAt=2000-01-01T00:00:00.0Z") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/IsNullFieldTypeTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/IsNullFieldTypeTest.kt index 40ec4d5..1ddf3fe 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/IsNullFieldTypeTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/IsNullFieldTypeTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class IsNullFieldTypeTest : BaseTest() { - @Test fun `should filter equals a Instant`() { mockMvc.get("/api/users?createdDate_is_null") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/LocalDateFieldTypeTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/LocalDateFieldTypeTest.kt index 2f6f669..8cccfce 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/LocalDateFieldTypeTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/LocalDateFieldTypeTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class LocalDateFieldTypeTest : BaseTest() { - @Test fun `should filter equals a date`() { mockMvc.get("/api/users?createdDate=2000-05-16") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/NumericFieldTypeTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/NumericFieldTypeTest.kt index 0153c6b..d148c53 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/NumericFieldTypeTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/fieldtype/NumericFieldTypeTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class NumericFieldTypeTest : BaseTest() { - @Test fun `should filter grater than`() { mockMvc.get("/api/users?age_gt=55") @@ -15,6 +14,7 @@ class NumericFieldTypeTest : BaseTest() { jsonPath("$", hasSize(1)) } } + @Test fun `should filter grater equals than`() { mockMvc.get("/api/users?age_ge=55") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/EqualFilterOperatorTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/EqualFilterOperatorTest.kt index b47d224..526f527 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/EqualFilterOperatorTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/EqualFilterOperatorTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class EqualFilterOperatorTest : BaseTest() { - @Test fun `should return all results when no filter present`() { mockMvc.get("/api/users") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/InFilterOperatorTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/InFilterOperatorTest.kt index a5595d6..5eaa3f9 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/InFilterOperatorTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/InFilterOperatorTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class InFilterOperatorTest : BaseTest() { - @Test fun `should filter in many values separated by comma`() { mockMvc.get("/api/users?age_in=19,21,31") diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/LikeFilterOperatorTest.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/LikeFilterOperatorTest.kt index fb4b83e..710c38e 100644 --- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/LikeFilterOperatorTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/operator/LikeFilterOperatorTest.kt @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test import org.springframework.test.web.servlet.get class LikeFilterOperatorTest : BaseTest() { - @Test fun `should filter like`() { mockMvc.get("/api/users?name_like=C.") diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterSqlWriterTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterSqlWriterTest.kt index 0f307ae..8b56c84 100644 --- a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterSqlWriterTest.kt +++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterSqlWriterTest.kt @@ -9,7 +9,6 @@ import java.time.Instant import java.time.LocalDate class R2dbcMagicFilterSqlWriterTest { - @Test fun `test sql writer equals string`() { val params = listOf("name" to "Joe").toMultiMap() diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt index e5daf8c..7f497a1 100644 --- a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt +++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt @@ -1,11 +1,11 @@ package io.github.verissimor.lib.r2dbcmagicfilter.domain import io.github.verissimor.lib.jpamagicfilter.Gender +import jakarta.persistence.Id +import jakarta.persistence.Table import java.time.Instant import java.time.LocalDate import java.util.UUID -import javax.persistence.Id -import javax.persistence.Table @Table(name = "app_user") data class ReactiveUser( @@ -18,5 +18,5 @@ data class ReactiveUser( val createdDate: LocalDate?, val createdAt: Instant?, val enabled: Boolean, - val uuid: UUID + val uuid: UUID, )