diff --git a/KSP/README.md b/KSP/README.md new file mode 100644 index 000000000..7f515cc7f --- /dev/null +++ b/KSP/README.md @@ -0,0 +1,56 @@ +# Supabase KSP compiler + +Currently only supports generating columns for `@Selectable` data classes. + +To install it, add the KSP Gradle plugin to your project: + +```kotlin +plugins { + id("com.google.devtools.ksp") version "2.1.21-2.0.2" //kotlinVersion-kspVersion +} +``` + +Then add the Supabase KSP compiler to your dependencies: + +[**JVM**](https://kotlinlang.org/docs/ksp-quickstart.html#add-a-processor): + +```kotlin +depdencies { + ksp("io.github.jan-tennert.supabase:ksp-compiler:VERSION") +} +``` + +[**Multiplatform**](https://kotlinlang.org/docs/ksp-multiplatform.html): + +```kotlin +kotlin { + //... + jvm() + androidTarget() + iosX64() + //... + + //Might need to add this if you cannot see generated code in your IDE + sourceSets.named("commonMain").configure { + kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") + } +} + +dependencies { + //Add KSP compiler to all targets that need processing + //Advised to use Gradle Version Catalogs + add("kspCommonMainMetadata", "io.github.jan-tennert.supabase:ksp-compiler:VERSION") + + //Note: If only the common target needs processing, you don't need the following lines + add("kspJvm", "io.github.jan-tennert.supabase:ksp-compiler:VERSION") + add("kspAndroid", "io.github.jan-tennert.supabase:ksp-compiler:VERSION") + add("kspIosX64", "io.github.jan-tennert.supabase:ksp-compiler:VERSION") +} + +//Might need to add this if you cannot see generated code in your IDE +project.tasks.withType(KotlinCompilationTask::class.java).configureEach { + if(name != "kspCommonMainKotlinMetadata") { + dependsOn("kspCommonMainKotlinMetadata") + } +} +``` \ No newline at end of file diff --git a/KSP/build.gradle.kts b/KSP/build.gradle.kts new file mode 100644 index 000000000..30bc9c2cd --- /dev/null +++ b/KSP/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + id(libs.plugins.kotlin.jvm.get().pluginId) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(libs.ksp) + implementation(libs.kotlin.poet) + implementation(libs.kotlin.poet.ksp) + implementation(project(":postgrest-kt")) +} + +tasks.named>("compileKotlin").configure { + compilerOptions.freeCompilerArgs.add("-opt-in=io.github.jan.supabase.annotations.SupabaseInternal") + compilerOptions.freeCompilerArgs.add("-opt-in=io.github.jan.supabase.annotations.SupabaseExperimental") +} \ No newline at end of file diff --git a/KSP/src/main/kotlin/io/github/jan/supabase/ksp/ColumnOptions.kt b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/ColumnOptions.kt new file mode 100644 index 000000000..1e2b799fa --- /dev/null +++ b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/ColumnOptions.kt @@ -0,0 +1,12 @@ +package io.github.jan.supabase.ksp + +data class ColumnOptions( + val alias: String, + val columnName: String?, + val isForeign: Boolean, + val jsonPath: List?, + val returnAsText: Boolean, + val function: String?, + val cast: String?, + val innerColumns: String, +) diff --git a/KSP/src/main/kotlin/io/github/jan/supabase/ksp/PrimitiveColumnType.kt b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/PrimitiveColumnType.kt new file mode 100644 index 000000000..197f49945 --- /dev/null +++ b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/PrimitiveColumnType.kt @@ -0,0 +1,25 @@ +package io.github.jan.supabase.ksp + +//Used for auto-casting, when no type is specified +val primitiveColumnTypes = mapOf( + "kotlin.String" to "text", + "kotlin.Int" to "int4", + "kotlin.Long" to "int8", + "kotlin.Float" to "float4", + "kotlin.Double" to "float8", + "kotlin.Boolean" to "bool", + "kotlin.Byte" to "int2", + "kotlin.Short" to "int2", + "kotlin.Char" to "char", + "kotlinx.datetime.Instant" to "timestamptz", + "kotlinx.datetime.LocalDateTime" to "timestamp", + "kotlin.uuid.Uuid" to "uuid", + "kotlinx.datetime.LocalTime" to "time", + "kotlinx.datetime.LocalDate" to "date", + "kotlinx.serialization.json.JsonElement" to "jsonb", + "kotlinx.serialization.json.JsonObject" to "jsonb", + "kotlinx.serialization.json.JsonArray" to "jsonb", + "kotlinx.serialization.json.JsonPrimitive" to "jsonb", +) + +fun isPrimitive(qualifiedName: String) = primitiveColumnTypes.containsKey(qualifiedName) \ No newline at end of file diff --git a/KSP/src/main/kotlin/io/github/jan/supabase/ksp/SelectableSymbolProcessor.kt b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/SelectableSymbolProcessor.kt new file mode 100644 index 000000000..90b02bb4d --- /dev/null +++ b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/SelectableSymbolProcessor.kt @@ -0,0 +1,232 @@ +package io.github.jan.supabase.ksp + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.google.devtools.ksp.symbol.KSNode +import com.google.devtools.ksp.symbol.KSValueParameter +import com.google.devtools.ksp.symbol.Modifier +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.postgrest.Postgrest +import io.github.jan.supabase.postgrest.annotations.ApplyFunction +import io.github.jan.supabase.postgrest.annotations.Cast +import io.github.jan.supabase.postgrest.annotations.ColumnName +import io.github.jan.supabase.postgrest.annotations.Foreign +import io.github.jan.supabase.postgrest.annotations.JsonPath +import io.github.jan.supabase.postgrest.annotations.Selectable + +class SelectableSymbolProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, + private val options: Map +) : SymbolProcessor { + + val packageName = options["selectablePackageName"] ?: "io.github.jan.supabase.postgrest" + val fileName = options["selectableFileName"] ?: "PostgrestColumns" + + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation(Selectable::class.java.name).filterIsInstance() + if (!symbols.iterator().hasNext()) return emptyList() + val types = hashMapOf() + symbols.forEach { symbol -> + val className = symbol.simpleName.asString() + val qualifiedName = symbol.simpleName.asString() //Kotlin JS doesn't support qualified names + if (!symbol.modifiers.contains(Modifier.DATA)) { + logger.error("The class $className is not a data class", symbol) + return emptyList() + } + val parameters = symbol.primaryConstructor?.parameters + if(parameters == null) { + logger.error("Primary constructor is null or has no parameter", symbol) + return emptyList() + } + val columns = parameters.joinToString(",") { processParameters(it, resolver) } + types[qualifiedName] = columns + } + writePostgrestExtensionFunction(types, symbols.mapNotNull { it.containingFile }.toList()) + return emptyList() + } + + private fun writePostgrestExtensionFunction( + columns: Map, + sources: List + ) { + val function = FunSpec.builder("addSelectableTypes") + .addKdoc(COMMENT) + .addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("kotlin.OptIn")).addMember("%T::class", + SupabaseInternal::class).build()) + .receiver(Postgrest.Config::class) + columns.forEach { (qualifiedName, columns) -> + function.addStatement("columnRegistry.registerColumns(\"$qualifiedName\", \"$columns\")") + } + val fileSpec = FileSpec.builder(packageName, fileName) + .addFunction(function.build()) + .addImport("io.github.jan.supabase.postgrest.annotations", "Selectable") + .build() + val stream = codeGenerator.createNewFile( + Dependencies(false, *sources.toTypedArray()), + packageName, + fileName, + extensionName = "kt" + ) + stream.bufferedWriter().use { + fileSpec.writeTo(it) + } + } + + private fun processParameters(parameter: KSValueParameter, resolver: Resolver): String { + val parameterClass = resolver.getClassDeclarationByName(parameter.type.resolve().declaration.qualifiedKSName) + requireNotNull(parameterClass) { "Parameter class is null" } + val innerColumns = if(!isPrimitive(parameterClass.qualifiedNameAsString)) { + val annotation = parameterClass.annotations.getAnnotationOrNull(Selectable::class.java.simpleName) + if(annotation == null) { + //Could be a JSON column or a custom type, so don't throw an error + logger.info("Type of parameter ${parameter.nameAsString} is not a primitive type and does not have @Selectable annotation", parameter) + "" + } else { + val columns = parameterClass.primaryConstructor?.parameters + if(columns == null) { + logger.error("Primary constructor of ${parameterClass.qualifiedName} is null or has no parameter", parameter) + return "" + } + columns.joinToString(",") { processParameters(it, resolver) } + } + } else "" + val alias = parameter.nameAsString + val columnName = parameter.annotations.getAnnotationOrNull(ColumnName::class.java.simpleName) + ?.arguments?.getParameterValue(ColumnName.NAME_PARAMETER_NAME) + val isForeign = parameter.annotations.getAnnotationOrNull(Foreign::class.java.simpleName) != null + val jsonPathArguments = parameter.annotations.getAnnotationOrNull(JsonPath::class.java.simpleName)?.arguments + val jsonPath = jsonPathArguments?.getParameterValue>(JsonPath.PATH_PARAMETER_NAME) + val returnAsText = jsonPathArguments?.getParameterValue(JsonPath.RETURN_AS_TEXT_PARAMETER_NAME) == true + val function = parameter.annotations.getAnnotationOrNull(ApplyFunction::class.java.simpleName) + ?.arguments?.getParameterValue(ApplyFunction.FUNCTION_PARAMETER_NAME) + val cast = parameter.annotations.getAnnotationOrNull(Cast::class.java.simpleName) + ?.arguments?.getParameterValue(Cast.TYPE_PARAMETER_NAME) + val options = ColumnOptions( + alias = alias, + columnName = columnName, + isForeign = isForeign, + jsonPath = jsonPath, + returnAsText = returnAsText, + function = function, + cast = cast, + innerColumns = innerColumns + ) + checkValidCombinations( + parameterName = parameter.nameAsString, + options = options, + symbol = parameter + ) + return buildColumns( + options = options, + parameterName = parameter.nameAsString, + symbol = parameter, + qualifiedTypeName = parameterClass.simpleName.asString() //Qualified names are not supported in Kotlin JS + ) + } + + private fun checkValidCombinations( + options: ColumnOptions, + parameterName: String, + symbol: KSNode + ) { + if(options.isForeign && options.jsonPath != null) { + logger.error("Parameter $parameterName can't have both @Foreign and @JsonPath annotation", symbol) + } + if(options.isForeign && options.function != null) { + logger.error("Parameter $parameterName can't have both @Foreign and @ApplyFunction annotation", symbol) + } + if(options.jsonPath != null && options.function != null) { + logger.error("Parameter $parameterName can't have both @JsonPath and @ApplyFunction annotation", symbol) + } + if(options.jsonPath != null && options.columnName == null) { + logger.error("Parameter $parameterName must have a @ColumnName annotation when using @JsonPath", symbol) + } + if(options.jsonPath != null && options.jsonPath.isEmpty()) { + logger.error("Parameter $parameterName must have at least one path in @JsonPath annotation", symbol) + } + } + + private fun buildColumns( + options: ColumnOptions, + parameterName: String, + qualifiedTypeName: String, + symbol: KSNode + ): String { + return buildString { + if(options.jsonPath != null) { + if(options.alias != options.jsonPath.last()) { + append("${options.alias}:") + } + append(buildJsonPath(options.columnName, options.jsonPath, options.returnAsText)) + return@buildString + } + + //If the column name is not provided, use the alias (parameter name) + if(options.columnName == null) { + append(options.alias) + } else { + append("${options.alias}:${options.columnName}") + } + if(options.isForeign) { + append("(${options.innerColumns})") + return@buildString + } + + //If a custom cast is provided, use it + if(options.cast != null && options.cast.isNotEmpty()) { + append("::${options.cast}") + } else if(options.cast != null) { //If cast is empty, try to auto-cast + val autoCast = primitiveColumnTypes[qualifiedTypeName] + if(autoCast != null) { + append("::$autoCast") + } else { + logger.error("Type of parameter $parameterName is not a primitive type and does not have an automatic cast type. Try to specify it manually.", symbol) + } + } + if(options.function != null) { + append(".${options.function}()") + } + } + } + + private fun buildJsonPath( + columnName: String?, + jsonPath: List, + returnAsText: Boolean + ): String { + val operator = if(returnAsText) "->>" else "->" + return buildString { + append(columnName) + if(jsonPath.size > 1) { + jsonPath.dropLast(1).forEach { + append("->$it") + } + } + append(operator) + append(jsonPath.last()) + } + } + + companion object { + + val COMMENT = """ + |Adds the types annotated with [Selectable] to the ColumnRegistry. Allows to use the automatically generated columns in the PostgrestQueryBuilder. + | + |This file is generated by the SelectableSymbolProcessor. + |Do not modify it manually. + """.trimMargin() + + } + +} \ No newline at end of file diff --git a/KSP/src/main/kotlin/io/github/jan/supabase/ksp/SelectableSymbolProcessorProvider.kt b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/SelectableSymbolProcessorProvider.kt new file mode 100644 index 000000000..0a2138cf8 --- /dev/null +++ b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/SelectableSymbolProcessorProvider.kt @@ -0,0 +1,13 @@ +package io.github.jan.supabase.ksp + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class SelectableSymbolProcessorProvider: SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return SelectableSymbolProcessor(environment.codeGenerator, environment.logger, environment.options) + } + +} \ No newline at end of file diff --git a/KSP/src/main/kotlin/io/github/jan/supabase/ksp/Utils.kt b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/Utils.kt new file mode 100644 index 000000000..2d7a56343 --- /dev/null +++ b/KSP/src/main/kotlin/io/github/jan/supabase/ksp/Utils.kt @@ -0,0 +1,28 @@ +package io.github.jan.supabase.ksp + +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSValueArgument +import com.google.devtools.ksp.symbol.KSValueParameter + +fun Sequence.getAnnotationOrNull(target: String): KSAnnotation? { + for (element in this) if (element.shortName.asString() == target) return element + return null +} + +fun List.getParameterValue(target: String): T { + return getParameterValueIfExist(target) ?: + throw NoSuchElementException("Sequence contains no element matching the predicate.") +} + +fun List.getParameterValueIfExist(target: String): T? { + for (element in this) if (element.name?.asString() == target) (element.value as? T)?.let { return it } + return null +} + +//Note these should never be null for our use case, but we still need to handle it +val KSValueParameter.nameAsString: String get() = name?.asString() ?: error("Parameter name is null") + +val KSDeclaration.qualifiedNameAsString get() = qualifiedKSName.asString() +val KSDeclaration.qualifiedKSName get() = qualifiedName ?: error("Qualified name is null") +val KSDeclaration.simpleNameAsString get() = simpleName.asString() \ No newline at end of file diff --git a/KSP/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/KSP/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 000000000..ac0dfdc8b --- /dev/null +++ b/KSP/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +io.github.jan.supabase.ksp.SelectableSymbolProcessorProvider \ No newline at end of file diff --git a/Postgrest/README.md b/Postgrest/README.md index c7c4f75a9..1ac6d4655 100644 --- a/Postgrest/README.md +++ b/Postgrest/README.md @@ -59,6 +59,9 @@ val supabase = createSupabaseClient( } ``` +> [!NOTE] +> Generating columns for `@Selectable` data classes requires the [KSP compiler](/KSP) to be installed. + # Usage See [Postgrest documentation](https://supabase.com/docs/reference/kotlin/select) for usage diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/ColumnRegistry.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/ColumnRegistry.kt new file mode 100644 index 000000000..f6ed98504 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/ColumnRegistry.kt @@ -0,0 +1,31 @@ +package io.github.jan.supabase.postgrest + +import io.github.jan.supabase.annotations.SupabaseInternal +import kotlin.reflect.KClass + +/** + * Registry used to map generated columns to a class + */ +@OptIn(ExperimentalSubclassOptIn::class) +@SupabaseInternal +@SubclassOptInRequired(SupabaseInternal::class) +interface ColumnRegistry { + + fun getColumns(kClass: KClass): String + + fun registerColumns(name: String, columns: String) + +} + +@SupabaseInternal +class MapColumnRegistry( + private val map: MutableMap = mutableMapOf() +): ColumnRegistry { + + override fun getColumns(kClass: KClass): String = map[kClass.simpleName] ?: error("No columns registered for $kClass") + + override fun registerColumns(name: String, columns: String) { + map[name] = columns + } + +} \ No newline at end of file diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt index 960ef4e75..438b4a05e 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt @@ -2,7 +2,8 @@ package io.github.jan.supabase.postgrest import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseSerializer -import io.github.jan.supabase.exceptions.HttpRequestException +import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.exceptions.RestException import io.github.jan.supabase.logging.SupabaseLogger import io.github.jan.supabase.plugins.CustomSerializationConfig import io.github.jan.supabase.plugins.CustomSerializationPlugin @@ -101,6 +102,7 @@ interface Postgrest : MainPlugin, CustomSerializationPlugin { data class Config( var defaultSchema: String = "public", var propertyConversionMethod: PropertyConversionMethod = PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE, + @property:SupabaseInternal var columnRegistry: ColumnRegistry = MapColumnRegistry() ): MainConfig(), CustomSerializationConfig { override var serializer: SupabaseSerializer? = null diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/PostgrestImpl.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/PostgrestImpl.kt index 0ade59c93..16a0a913b 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/PostgrestImpl.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/PostgrestImpl.kt @@ -56,7 +56,7 @@ internal class PostgrestImpl(override val supabaseClient: SupabaseClient, overri override suspend fun rpc(function: String, request: RpcRequestBuilder.() -> Unit): PostgrestResult = rpcRequest(function, null, request) private suspend fun rpcRequest(function: String, body: JsonObject? = null, request: RpcRequestBuilder.() -> Unit): PostgrestResult { - val requestBuilder = RpcRequestBuilder(config.defaultSchema, config.propertyConversionMethod).apply(request) + val requestBuilder = RpcRequestBuilder(config.defaultSchema, config).apply(request) val urlParams = buildMap { putAll(requestBuilder.params.mapToFirstValue()) if(requestBuilder.method != RpcMethod.POST && body != null) { diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/ApplyFunction.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/ApplyFunction.kt new file mode 100644 index 000000000..ab0648840 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/ApplyFunction.kt @@ -0,0 +1,42 @@ +package io.github.jan.supabase.postgrest.annotations + +import io.github.jan.supabase.annotations.SupabaseInternal + +/** + * Annotation to apply a function to a column in a PostgREST query. + * @param function The function to apply to the column. + */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +annotation class ApplyFunction(val function: String) { + + companion object { + @SupabaseInternal const val FUNCTION_PARAMETER_NAME = "function" + + /** + * Apply the `avg` function to a column. + */ + const val AVG = "avg" + + /** + * Apply the `count` function to a column. + */ + const val COUNT = "count" + + /** + * Apply the `max` function to a column. + */ + const val MAX = "max" + + /** + * Apply the `min` function to a column. + */ + const val MIN = "min" + + /** + * Apply the `sum` function to a column. + */ + const val SUM = "sum" + } + +} diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Cast.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Cast.kt new file mode 100644 index 000000000..062c68751 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Cast.kt @@ -0,0 +1,17 @@ +package io.github.jan.supabase.postgrest.annotations + +import io.github.jan.supabase.annotations.SupabaseInternal + +/** + * Annotation to cast a column in a PostgREST query. + * @param type The type to cast the column to. If empty, the type will be inferred from the parameter type. For example, if the parameter is of type [String], the column will be cast to `text`. For all supported auto-casts see the primitive types in [Selectable]. + */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +annotation class Cast(val type: String = "") { + + companion object { + @SupabaseInternal const val TYPE_PARAMETER_NAME = "type" + } + +} diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/ColumnName.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/ColumnName.kt new file mode 100644 index 000000000..f6deb7f3e --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/ColumnName.kt @@ -0,0 +1,19 @@ +package io.github.jan.supabase.postgrest.annotations + +import io.github.jan.supabase.annotations.SupabaseInternal + +/** + * Annotation to specify the name of a column in a PostgREST query. + * + * If this annotation is not present, the name of the column will be inferred from the parameter name. + * @param name The name of the column. + */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +annotation class ColumnName(val name: String) { + + companion object { + @SupabaseInternal const val NAME_PARAMETER_NAME = "name" + } + +} diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Foreign.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Foreign.kt new file mode 100644 index 000000000..0ba826b75 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Foreign.kt @@ -0,0 +1,10 @@ +package io.github.jan.supabase.postgrest.annotations + +/** + * Annotation to specify that a column is a foreign key in a PostgREST query. + * + * This annotation may be used in combination with [ColumnName] to specify the name of the foreign key column. The type of the parameter must be annotated with [Selectable]. + */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +annotation class Foreign diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/JsonPath.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/JsonPath.kt new file mode 100644 index 000000000..1dafbfbf9 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/JsonPath.kt @@ -0,0 +1,44 @@ +package io.github.jan.supabase.postgrest.annotations + +import io.github.jan.supabase.annotations.SupabaseInternal + +/** + * Annotation to specify a JSON path in a PostgREST query. + * + * Example: + * Table with a JSON column `data`: + * ```json + * { + * "id": 1, + * "nested": { + * "name": "John" + * } + * } + * ``` + * ```kotlin + * @Selectable + * data class Example( + * @ColumnName("data") + * @JsonPath("id") + * val id: Int, + * + * @ColumnName("data") + * @JsonPath("nested", "name") + * val name: String + * ) + * ``` + * + * @param path The path to the JSON property. At least one path must be specified. + * @param returnAsText Whether to return the JSON property as text. If `true`, the JSON property will be returned as a string. If `false`, the JSON property will be returned as JSON. + * + */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +annotation class JsonPath(vararg val path: String, val returnAsText: Boolean = false) { + + companion object { + @SupabaseInternal const val PATH_PARAMETER_NAME = "path" + @SupabaseInternal const val RETURN_AS_TEXT_PARAMETER_NAME = "returnAsText" + } + +} diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Selectable.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Selectable.kt new file mode 100644 index 000000000..f82f19e75 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/annotations/Selectable.kt @@ -0,0 +1,54 @@ +package io.github.jan.supabase.postgrest.annotations + +import io.github.jan.supabase.annotations.SupabaseExperimental +import io.github.jan.supabase.postgrest.query.PostgrestQueryBuilder +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlin.uuid.Uuid + +/** + * Annotates a class as selectable. + * + * When using the **ksp-compiler** this annotation will be processed and columns for [PostgrestQueryBuilder.select] will be generated. + * + * The columns can be accessed via the generated extension property for the companion object of the class. + * + * **Note:** + * - All classes annotated with this annotation must have a companion object, which can be empty, and must be a data class + * - All parameters in the primary constructor must a primitive type¹, a type that is also annotated with [Selectable] or a serializable type. + * - Parameters may be annotated with [ColumnName], [ApplyFunction], [Cast], [JsonPath], [Foreign]. + * + * ¹: Available primitive types are: [String], [Int], [Long], [Float], [Double], [Boolean], [Byte], [Short], [Char], [Instant], [LocalDateTime], [Uuid], [LocalTime], [LocalDate], [JsonElement], [JsonObject], [JsonArray], [JsonPrimitive] + * + * Example usage: + * ```kotlin + * @Selectable + * data class User( + * val id: Int, + * val name: String, + * val age: Int + * //... + * ) { + * companion object + * } + * + * //Usage + * val users: List = supabase.from("users").select(User.columns).decodeList() + * ``` + * @see ApplyFunction + * @see Cast + * @see ColumnName + * @see Foreign + * @see JsonPath + * @see PostgrestQueryBuilder.select + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +@SupabaseExperimental +annotation class Selectable \ No newline at end of file diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestBuilderExtension.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestBuilderExtension.kt new file mode 100644 index 000000000..adb9c3889 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestBuilderExtension.kt @@ -0,0 +1,43 @@ +package io.github.jan.supabase.postgrest.query + +import io.github.jan.supabase.annotations.SupabaseExperimental +import io.github.jan.supabase.auth.PostgrestFilterDSL +import io.github.jan.supabase.exceptions.HttpRequestException +import io.github.jan.supabase.exceptions.RestException +import io.github.jan.supabase.postgrest.annotations.Selectable +import io.github.jan.supabase.postgrest.query.request.SelectRequestBuilder +import io.github.jan.supabase.postgrest.result.PostgrestResult +import io.ktor.client.plugins.HttpRequestTimeoutException + +/** + * Executes vertical filtering with select on [PostgrestQueryBuilder.table]. + * + * - This method is a shorthand for [select] with the columns automatically determined by the [T] type. + * - [T] must be marked with [Selectable] and the `ksp-compiler` KSP dependency must be added to the project (via the KSP Gradle plugin). + * + * @param request Additional configurations for the request including filters + * @return PostgrestResult which is either an error, an empty JsonArray or the data you requested as an JsonArray + * @throws RestException or one of its subclasses if receiving an error response + * @throws HttpRequestTimeoutException if the request timed out + * @throws HttpRequestException on network related issues + */ +@SupabaseExperimental +suspend inline fun PostgrestQueryBuilder.select( + request: @PostgrestFilterDSL SelectRequestBuilder.() -> Unit = {} +): PostgrestResult { + val registry = postgrest.config.columnRegistry + val columns = registry.getColumns(T::class) + return select(Columns.raw(columns), request) +} + +/** + * Return `data` after the query has been executed. + * + * - This method is a shorthand for [select] with the columns automatically determined by the [T] type. + * - [T] must be marked with [Selectable] and the `ksp-compiler` KSP dependency must be added to the project (via the KSP Gradle plugin). + */ +@SupabaseExperimental +inline fun PostgrestRequestBuilder.select() { + val columns = config.columnRegistry.getColumns(T::class) + select(Columns.raw(columns)) +} diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestQueryBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestQueryBuilder.kt index 2b0d76624..f03a73cbf 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestQueryBuilder.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestQueryBuilder.kt @@ -43,7 +43,9 @@ class PostgrestQueryBuilder( columns: Columns = Columns.ALL, request: @PostgrestFilterDSL SelectRequestBuilder.() -> Unit = {} ): PostgrestResult { - val requestBuilder = SelectRequestBuilder(postgrest.config.propertyConversionMethod).apply { + val requestBuilder = SelectRequestBuilder( + postgrest.config + ).apply { request(); params["select"] = listOf(columns.value) } val selectRequest = SelectRequest( @@ -75,7 +77,9 @@ class PostgrestQueryBuilder( values: List, request: UpsertRequestBuilder.() -> Unit = {} ): PostgrestResult { - val requestBuilder = UpsertRequestBuilder(postgrest.config.propertyConversionMethod).apply(request) + val requestBuilder = UpsertRequestBuilder( + postgrest.config + ).apply(request) val body = postgrest.serializer.encodeToJsonElement(values).jsonArray val columns = body.map { it.jsonObject.keys }.flatten().distinct() if(columns.isNotEmpty()) requestBuilder.params["columns"] = listOf(columns.joinToString(",")) @@ -129,7 +133,9 @@ class PostgrestQueryBuilder( values: List, request: InsertRequestBuilder.() -> Unit = {} ): PostgrestResult { - val requestBuilder = InsertRequestBuilder(postgrest.config.propertyConversionMethod).apply(request) + val requestBuilder = InsertRequestBuilder( + postgrest.config + ).apply(request) val body = postgrest.serializer.encodeToJsonElement(values).jsonArray val columns = body.map { it.jsonObject.keys }.flatten().distinct() if(columns.isNotEmpty()) requestBuilder.params["columns"] = listOf(columns.joinToString(",")) @@ -174,7 +180,7 @@ class PostgrestQueryBuilder( crossinline update: PostgrestUpdate.() -> Unit = {}, request: PostgrestRequestBuilder.() -> Unit = {} ): PostgrestResult { - val requestBuilder = PostgrestRequestBuilder(postgrest.config.propertyConversionMethod).apply(request) + val requestBuilder = newRequestBuilder(request) val updateRequest = UpdateRequest( body = buildPostgrestUpdate(postgrest.config.propertyConversionMethod, postgrest.serializer, update), returning = requestBuilder.returning, @@ -201,7 +207,7 @@ class PostgrestQueryBuilder( value: T, request: PostgrestRequestBuilder.() -> Unit = {} ): PostgrestResult { - val requestBuilder = PostgrestRequestBuilder(postgrest.config.propertyConversionMethod).apply(request) + val requestBuilder = newRequestBuilder(request) val updateRequest = UpdateRequest( returning = requestBuilder.returning, count = requestBuilder.count, @@ -226,7 +232,7 @@ class PostgrestQueryBuilder( suspend inline fun delete( request: PostgrestRequestBuilder.() -> Unit = {} ): PostgrestResult { - val requestBuilder = PostgrestRequestBuilder(postgrest.config.propertyConversionMethod).apply(request) + val requestBuilder = newRequestBuilder(request) val deleteRequest = DeleteRequest( returning = requestBuilder.returning, count = requestBuilder.count, @@ -237,6 +243,11 @@ class PostgrestQueryBuilder( return RestRequestExecutor.execute(postgrest = postgrest, path = table, request = deleteRequest) } + @PublishedApi + internal inline fun newRequestBuilder(request: PostgrestRequestBuilder.() -> Unit = {}) = PostgrestRequestBuilder( + postgrest.config + ).apply(request) + companion object { const val HEADER_PREFER = "Prefer" } diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestRequestBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestRequestBuilder.kt index 656d016cb..c58a4786d 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestRequestBuilder.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/PostgrestRequestBuilder.kt @@ -3,7 +3,7 @@ package io.github.jan.supabase.postgrest.query import io.github.jan.supabase.annotations.SupabaseExperimental import io.github.jan.supabase.auth.PostgrestFilterDSL -import io.github.jan.supabase.postgrest.PropertyConversionMethod +import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.query.filter.PostgrestFilterBuilder import io.github.jan.supabase.postgrest.result.PostgrestResult import io.ktor.http.HeadersBuilder @@ -14,7 +14,9 @@ import kotlin.js.JsName * A builder for Postgrest requests. */ @PostgrestFilterDSL -open class PostgrestRequestBuilder(@PublishedApi internal val propertyConversionMethod: PropertyConversionMethod) { +open class PostgrestRequestBuilder( + @PublishedApi internal val config: Postgrest.Config +) { /** * The [Count] algorithm to use to count rows in the table or view. @@ -26,7 +28,7 @@ open class PostgrestRequestBuilder(@PublishedApi internal val propertyConversion * The [Returning] option to use. */ var returning: Returning = Returning.Minimal - private set + internal set @SupabaseExperimental val params: MutableMap> = mutableMapOf() @SupabaseExperimental val headers: HeadersBuilder = HeadersBuilder() @@ -156,7 +158,7 @@ open class PostgrestRequestBuilder(@PublishedApi internal val propertyConversion * @param block The filter block */ inline fun filter(block: @PostgrestFilterDSL PostgrestFilterBuilder.() -> Unit) { - val filter = PostgrestFilterBuilder(propertyConversionMethod, params) + val filter = PostgrestFilterBuilder(config.propertyConversionMethod, params) filter.block() } diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/InsertRequestBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/InsertRequestBuilder.kt index 4c3f5ff28..2e78cc299 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/InsertRequestBuilder.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/InsertRequestBuilder.kt @@ -1,13 +1,13 @@ package io.github.jan.supabase.postgrest.query.request -import io.github.jan.supabase.postgrest.PropertyConversionMethod +import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.query.PostgrestQueryBuilder import io.github.jan.supabase.postgrest.query.PostgrestRequestBuilder /** * Request builder for [PostgrestQueryBuilder.insert] */ -open class InsertRequestBuilder(propertyConversionMethod: PropertyConversionMethod): PostgrestRequestBuilder(propertyConversionMethod) { +open class InsertRequestBuilder(config: Postgrest.Config): PostgrestRequestBuilder(config) { /** * Make missing fields default to `null`. diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/RpcRequestBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/RpcRequestBuilder.kt index 5f631cf98..0bbf3caad 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/RpcRequestBuilder.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/RpcRequestBuilder.kt @@ -1,14 +1,16 @@ package io.github.jan.supabase.postgrest.query.request import io.github.jan.supabase.postgrest.Postgrest -import io.github.jan.supabase.postgrest.PropertyConversionMethod import io.github.jan.supabase.postgrest.RpcMethod import io.github.jan.supabase.postgrest.query.PostgrestRequestBuilder /** * Request builder for [Postgrest.rpc] */ -class RpcRequestBuilder(defaultSchema: String, propertyConversionMethod: PropertyConversionMethod): PostgrestRequestBuilder(propertyConversionMethod) { +class RpcRequestBuilder( + defaultSchema: String, + config: Postgrest.Config +): PostgrestRequestBuilder(config) { /** * The HTTP method to use. Default is POST diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/SelectRequestBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/SelectRequestBuilder.kt index 1ac90fac3..3249f0077 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/SelectRequestBuilder.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/SelectRequestBuilder.kt @@ -1,13 +1,15 @@ package io.github.jan.supabase.postgrest.query.request -import io.github.jan.supabase.postgrest.PropertyConversionMethod +import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.query.PostgrestQueryBuilder import io.github.jan.supabase.postgrest.query.PostgrestRequestBuilder /** * Request builder for [PostgrestQueryBuilder.select] */ -class SelectRequestBuilder(propertyConversionMethod: PropertyConversionMethod): PostgrestRequestBuilder(propertyConversionMethod) { +class SelectRequestBuilder( + config: Postgrest.Config +): PostgrestRequestBuilder(config) { /** * If true, no body will be returned. Useful when using count. diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/UpsertRequestBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/UpsertRequestBuilder.kt index 024e00338..2356d0938 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/UpsertRequestBuilder.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/request/UpsertRequestBuilder.kt @@ -1,12 +1,14 @@ package io.github.jan.supabase.postgrest.query.request -import io.github.jan.supabase.postgrest.PropertyConversionMethod +import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.query.PostgrestQueryBuilder /** * Request builder for [PostgrestQueryBuilder.upsert] */ -class UpsertRequestBuilder(propertyConversionMethod: PropertyConversionMethod): InsertRequestBuilder(propertyConversionMethod) { +class UpsertRequestBuilder( + config: Postgrest.Config +): InsertRequestBuilder(config) { /** * Comma-separated UNIQUE column(s) to specify how diff --git a/Postgrest/src/commonTest/kotlin/ColumnRegistryTest.kt b/Postgrest/src/commonTest/kotlin/ColumnRegistryTest.kt new file mode 100644 index 000000000..d39ec3b17 --- /dev/null +++ b/Postgrest/src/commonTest/kotlin/ColumnRegistryTest.kt @@ -0,0 +1,14 @@ +import io.github.jan.supabase.postgrest.MapColumnRegistry +import kotlin.test.Test +import kotlin.test.assertEquals + +class ColumnRegistryTest { + + @Test + fun testColumnRegistry() { + val registry = MapColumnRegistry() + registry.registerColumns("TestType", "column1, column2") + assertEquals("column1, column2", registry.getColumns(TestType::class)) + } + +} \ No newline at end of file diff --git a/Postgrest/src/commonTest/kotlin/PostgrestRequestBuilderTest.kt b/Postgrest/src/commonTest/kotlin/PostgrestRequestBuilderTest.kt index 1ddc65858..243db63c3 100644 --- a/Postgrest/src/commonTest/kotlin/PostgrestRequestBuilderTest.kt +++ b/Postgrest/src/commonTest/kotlin/PostgrestRequestBuilderTest.kt @@ -1,9 +1,13 @@ +import io.github.jan.supabase.postgrest.MapColumnRegistry +import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.query.Count import io.github.jan.supabase.postgrest.query.Order import io.github.jan.supabase.postgrest.query.Returning +import io.github.jan.supabase.postgrest.query.select import io.ktor.http.HttpHeaders import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs class PostgrestRequestBuilderTest { @@ -23,6 +27,19 @@ class PostgrestRequestBuilderTest { assertEquals(Returning.Representation(), request.returning) } + @Test + fun testSelectWithSelectable() { + val columnRegistry = MapColumnRegistry() + columnRegistry.registerColumns("TestType", "column1, column2") + val request = postgrestRequest( + config = Postgrest.Config(columnRegistry = columnRegistry) + ) { + select() + } + assertIs(request.returning) + assertEquals("column1,column2", (request.returning as Returning.Representation).columns.value) + } + @Test fun testSingle() { val request = postgrestRequest { diff --git a/Postgrest/src/commonTest/kotlin/PostgrestTest.kt b/Postgrest/src/commonTest/kotlin/PostgrestTest.kt index 13396d1b6..908e02fd4 100644 --- a/Postgrest/src/commonTest/kotlin/PostgrestTest.kt +++ b/Postgrest/src/commonTest/kotlin/PostgrestTest.kt @@ -7,6 +7,7 @@ import io.github.jan.supabase.postgrest.postgrest import io.github.jan.supabase.postgrest.query.Columns import io.github.jan.supabase.postgrest.query.request.InsertRequestBuilder import io.github.jan.supabase.postgrest.query.request.UpsertRequestBuilder +import io.github.jan.supabase.postgrest.query.select import io.github.jan.supabase.postgrest.result.PostgrestResult import io.github.jan.supabase.testing.assertMethodIs import io.github.jan.supabase.testing.assertPathIs @@ -57,6 +58,28 @@ class PostgrestTest { ) } + @Test + fun testSelectWithSelectableType() { + testClient( + postgrestConfig = { + columnRegistry.registerColumns("TestType", "column1,column2") + }, + request = { table -> + from(table).select { + headers["custom"] = "value" + params["custom"] = listOf("value") + } + }, + requestHandler = { + assertMethodIs(HttpMethod.Get, it.method) + assertEquals("column1,column2", it.url.parameters["select"]) + assertEquals("value", it.headers["custom"]) + assertEquals("value", it.url.parameters["custom"]) + respond("") + } + ) + } + @Test fun testSelectSchema() { val columns = Columns.list("column1", "column2") @@ -362,11 +385,16 @@ class PostgrestTest { private fun testClient( table: String = "table", + postgrestConfig: Postgrest.Config.() -> Unit = {}, request: suspend SupabaseClient.(table: String) -> PostgrestResult, requestHandler: suspend MockRequestHandleScope.(HttpRequestData) -> HttpResponseData = { respond("")}, ) { val supabase = createMockedSupabaseClient( - configuration = configureClient + configuration = { + install(Postgrest) { + postgrestConfig() + } + } ) { assertPathIs("/$table", it.url.pathAfterVersion()) requestHandler(it) diff --git a/Postgrest/src/commonTest/kotlin/TestType.kt b/Postgrest/src/commonTest/kotlin/TestType.kt new file mode 100644 index 000000000..cb4aa56c8 --- /dev/null +++ b/Postgrest/src/commonTest/kotlin/TestType.kt @@ -0,0 +1 @@ +data class TestType(val column1: String, val column2: String) diff --git a/Postgrest/src/commonTest/kotlin/Utils.kt b/Postgrest/src/commonTest/kotlin/Utils.kt index b1db90d84..dea02a807 100644 --- a/Postgrest/src/commonTest/kotlin/Utils.kt +++ b/Postgrest/src/commonTest/kotlin/Utils.kt @@ -1,10 +1,13 @@ import io.github.jan.supabase.annotations.SupabaseInternal -import io.github.jan.supabase.postgrest.PropertyConversionMethod +import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.query.PostgrestRequestBuilder @SupabaseInternal -inline fun postgrestRequest(propertyConversionMethod: PropertyConversionMethod = PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE, block: PostgrestRequestBuilder.() -> Unit): PostgrestRequestBuilder { - val filter = PostgrestRequestBuilder(propertyConversionMethod) +inline fun postgrestRequest( + config: Postgrest.Config = Postgrest.Config(), + block: PostgrestRequestBuilder.() -> Unit +): PostgrestRequestBuilder { + val filter = PostgrestRequestBuilder(config) filter.block() return filter } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Modules.kt b/buildSrc/src/main/kotlin/Modules.kt index 4cf5bdc56..82402c7d8 100644 --- a/buildSrc/src/main/kotlin/Modules.kt +++ b/buildSrc/src/main/kotlin/Modules.kt @@ -1,3 +1,4 @@ +import org.gradle.kotlin.dsl.DependencyHandlerScope import org.jetbrains.compose.desktop.DesktopExtension import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler diff --git a/detekt.yml b/detekt.yml index 0762d3878..cd4ebe1c0 100644 --- a/detekt.yml +++ b/detekt.yml @@ -75,7 +75,7 @@ comments: allowParamOnConstructorProperties: false UndocumentedPublicClass: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/mingwX64Test/**', '**/macosTest/**', '**/appleTest/**', '**/linuxTest/**', '**/wasmJsTest/**'] + excludes: ['**/KSP/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/mingwX64Test/**', '**/macosTest/**', '**/appleTest/**', '**/linuxTest/**', '**/wasmJsTest/**'] searchInNestedClass: true searchInInnerClass: true searchInInnerObject: false @@ -86,14 +86,14 @@ comments: - 'SupabaseExperimental' UndocumentedPublicFunction: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/mingwX64Test/**', '**/macosTest/**', '**/appleTest/**', '**/linuxTest/**', '**/wasmJsTest/**'] + excludes: ['**/KSP/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/mingwX64Test/**', '**/macosTest/**', '**/appleTest/**', '**/linuxTest/**', '**/wasmJsTest/**'] searchProtectedFunction: false ignoreAnnotated: - 'SupabaseInternal' - 'SupabaseExperimental' UndocumentedPublicProperty: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/mingwX64Test/**', '**/macosTest/**', '**/appleTest/**', '**/linuxTest/**', '**/wasmJsTest/**'] + excludes: ['**/KSP/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/mingwX64Test/**', '**/macosTest/**', '**/appleTest/**', '**/linuxTest/**', '**/wasmJsTest/**'] searchProtectedProperty: false ignoreAnnotated: - 'SupabaseInternal' diff --git a/gradle.properties b/gradle.properties index 4d73b240f..9c32c898c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -supabase-version = 3.2.0-rc-1 +supabase-version = 3.2.0-ksp-b1 base-group = io.github.jan-tennert.supabase diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 757a1e760..b0ab808a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,9 +35,12 @@ androidx-lifecycle = "2.9.1" filekit = "0.8.8" kotlinx-browser = "0.3" secure-random = "0.5.1" +ksp = "2.1.21-2.0.2" +kotlin-poet = "2.0.0" [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } kotlinx-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } @@ -54,6 +57,9 @@ kotlinx-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomi power-assert = { id = "org.jetbrains.kotlin.plugin.power-assert", version.ref = "kotlin" } [libraries] +ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } +kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet" } +kotlin-poet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet" } kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinx-browser" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b86223b1..e5ce392aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ plugins { // Main Modules include("Auth") include("Postgrest") +include("KSP") include("Storage") include("Realtime") include("Functions") @@ -51,6 +52,7 @@ project(":Storage").name = "storage-kt" project(":Realtime").name = "realtime-kt" project(":Functions").name = "functions-kt" project(":Supabase").name = "supabase-kt" +project(":KSP").name = "ksp-compiler" project(":plugins:ApolloGraphQL").name = "apollo-graphql" project(":plugins:ComposeAuth").name = "compose-auth" project(":plugins:ComposeAuthUI").name = "compose-auth-ui"