From a5afd32df6d118f711d2e37b367f484af376f49c Mon Sep 17 00:00:00 2001 From: Verissimo Ribeiro Date: Sat, 13 Jan 2024 13:00:39 +0000 Subject: [PATCH] chore(spring): upgrade to spring v3 --- README.md | 11 +- build.gradle.kts | 30 ++- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 31 ++- gradlew.bat | 15 +- .../verissimor/lib/fieldparser/FieldParser.kt | 51 ++-- .../lib/fieldparser/domain/CombineOperator.kt | 3 +- .../lib/fieldparser/domain/FieldType.kt | 44 ++-- .../lib/fieldparser/domain/FilterOperator.kt | 2 +- .../lib/fieldparser/domain/ParsedField.kt | 1 - .../lib/fieldparser/domain/ValueParser.kt | 86 ++++--- .../lib/jpamagicfilter/FieldParser.kt | 44 ++-- .../lib/jpamagicfilter/MagicFilter.kt | 26 ++- .../jpamagicfilter/MagicFilterConfigurer.kt | 7 +- .../lib/jpamagicfilter/PredicateParser.kt | 137 +++++++---- .../lib/jpamagicfilter/domain/DbFeatures.kt | 2 +- .../lib/jpamagicfilter/domain/ParsedField.kt | 5 +- .../lib/r2dbcmagicfilter/KotlinUtil.kt | 17 +- .../lib/r2dbcmagicfilter/R2dbcMagicFilter.kt | 122 +++++----- .../R2dbcMagicFilterConfigurer.kt | 6 +- .../r2dbcmagicfilter/R2dbcPredicateParser.kt | 181 +++++++------- .../sqlwriter/R2dbcSqlWriter.kt | 220 +++++++++--------- .../r2dbcmagicfilter/sqlwriter/SqlBinder.kt | 3 +- .../FieldParserCombineOperatorTest.kt | 1 - .../fieldparser/FieldParserFieldTypeTest.kt | 1 - .../lib/fieldparser/FieldParserGroupTest.kt | 1 - .../fieldparser/FieldParserOperatorTest.kt | 1 - .../verissimor/lib/jpamagicfilter/BaseTest.kt | 1 - .../lib/jpamagicfilter/CombineOperatorTest.kt | 1 - .../lib/jpamagicfilter/DemoApplication.kt | 91 ++++---- .../lib/jpamagicfilter/NestedClassTest.kt | 1 - .../fieldtype/InstantFieldTypeTest.kt | 1 - .../fieldtype/IsNullFieldTypeTest.kt | 1 - .../fieldtype/LocalDateFieldTypeTest.kt | 1 - .../fieldtype/NumericFieldTypeTest.kt | 2 +- .../operator/EqualFilterOperatorTest.kt | 1 - .../operator/InFilterOperatorTest.kt | 1 - .../operator/LikeFilterOperatorTest.kt | 1 - .../R2dbcMagicFilterSqlWriterTest.kt | 1 - .../r2dbcmagicfilter/domain/ReactiveUser.kt | 6 +- 41 files changed, 674 insertions(+), 487 deletions(-) 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..f417f1f 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.1-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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 44391 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%0D+ym=_w*d`k$@iM%aZm>(FB zFT}u)TP%ySs`RyJx#=oxe&Idagc(F$Sv(5*pWd?HYll`LXi*p`s9;8=FNKjA$zuDap z1iJ4_{GzB&FY7I^Z=V2WPV}duAN#+$7*g>}M-8M^ZIIB>-^X-D90KS2pYi;cJpI2t z9+!d$Z~~_e2!65t2V*tx>VN>IOBPZ~3v6t}2DR0RaAz+Y0&>nb^fj?~75%>lATlV_DagaSI48TP5e#m z`NUsBkeiDL|BMZ$&97r4JO2|SVnYrr+W2*{e!H;xn_8U}Ah!z{@Mwehm%07(!p~pV geSU+(twG={1R5&vkYyYSN)YnO$AzrH2Y-C~e^Sofwg3PC delta 40281 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 /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, )