diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/RawQueryHelper.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/RawQueryHelper.kt new file mode 100644 index 000000000..263e44fc7 --- /dev/null +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/RawQueryHelper.kt @@ -0,0 +1,170 @@ +package io.github.jan.supabase.postgrest.query + +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementNames +import kotlinx.serialization.serializer + +object RawQueryHelper { + /** + * Generates a query string for a given class, including multiple objects with their properties. + * Use to query multiple objects with foreign keys + * + * @return A string representing the query, e.g., "className(prop1, prop2)" or "prop1, prop2: prop2(subProp)". + * + * @throws kotlinx.serialization.SerializationException If the class [T] is not + * serializable or lacks a serializer. + * + * Example usage: + * ``` + * @Serializable + * private data class MessageDto( + * @SerialName("content") val content: String, + * @SerialName("recipient")val recipient: UserDto, + * @SerialName("sender") val createdBy: UserDto, + * @SerialName("created_at") val createdAt: String, + * ) + * + * @Serializable + * private data class UserDto( + * @SerialName("id") val id: String, + * @SerialName("username") val username: String, + * @SerialName("email") val email: String, + * ) + *``` + * ``` + * val query = RawQueryHelper.queryWithMultipleForeignKeys() + * // Returns + * """ + * content, + * recipient: recipient(id, username, email), + * sender: sender(id, username, email), + * created_at + * """ + * // Perform query with Postgrest + * val columns = Columns.raw(query) + * val messages = postgrest.from("messages") + * .select( + * columns = columns + * ) { + * filter { + * eq(queryParam, queryValue) + * } + * }.decodeList() + * ``` + */ + inline fun queryWithMultipleForeignKeys(): String { + val lowercasedClassName = T::class.simpleName?.lowercase() + val descriptor: SerialDescriptor = serializer().descriptor + return buildKeyString(descriptor, lowercasedClassName ?: "") + } + + /** + * Generates a query string for a given class, including properties and one object that + * foreign key refers to. + * Used to query one object request with one foreign key + * + * @param T The reified type parameter representing the class to generate a query string + * for. The class must be annotated with [kotlinx.serialization.Serializable] + * for serialization support. + * @param customizedClassName An optional string to override the name used for nested + * objects in the query. If null, the original property name + * is used. + * @return A formatted query string listing the properties of the class. Nested classes + * are represented with their properties in a nested format (e.g., + * "name: name(subProp)"), and properties are separated by commas and newlines, + * with no trailing comma. + * + * @throws kotlinx.serialization.SerializationException If the class [T] is not + * serializable or lacks a serializer. + * + * Example usage: + * ``` + * @Serializable + * data class UserDto( + * @SerialName("id") val id: String, + * @SerialName("username") val username: String + * ) + * + * @Serializable + * data class System( + * @SerialName("name") val name: String, + * @SerialName("owner") val owner: UserDto + * ) + *``` + *``` + * val query = RawQueryHelper.queryWithForeignKey() + * // Returns: + * """ + * name, + * owner: owner(id, username) + * """ + * + * val customQuery = RawQueryHelper.queryWithForeignKey("user") + * // Returns: + * """ + * name, + * user(id, username) + * """ + * // Perform query with Postgrest + * val columns = Columns.raw(query) + * val systems = postgrest.from("systems") + * .select( + * columns = columns + * ) { + * filter { + * eq(queryParam, queryValue) + * } + * }.decodeList() + *``` + */ + inline fun queryWithForeignKey(customizedClassName: String? = null): String { + val descriptor: SerialDescriptor = serializer().descriptor + val properties = descriptor.elementNames.mapIndexed { index, name -> + val elementDescriptor = descriptor.getElementDescriptor(index) + if (elementDescriptor.kind is StructureKind.CLASS && !elementDescriptor.isInline) { + val updatedName = customizedClassName ?: name + "$updatedName${buildKeyString(elementDescriptor, "")}" + } else { + name + } + } + val result = properties.mapIndexed { index, property -> + if (index < properties.size - 1) "$property," else property + }.joinToString("\n") + result.removeSuffix(",") + return result + } + + fun buildKeyString(descriptor: SerialDescriptor, className: String): String { + val containsNonInlineClass = (0 until descriptor.elementsCount).any { index -> + val elementDescriptor = descriptor.getElementDescriptor(index) + elementDescriptor.kind is StructureKind.CLASS && !elementDescriptor.isInline + } + + if (!containsNonInlineClass) { + val propertyNames = (0 until descriptor.elementsCount).joinToString(", ") { index -> + descriptor.getElementName(index) + } + return "$className($propertyNames)" + } else { + val properties = descriptor.elementNames.mapIndexed { index, name -> + val elementDescriptor = descriptor.getElementDescriptor(index) + if (elementDescriptor.kind is StructureKind.CLASS && !elementDescriptor.isInline) { + // For nested classes, the prefix is "elementName: elementName" + // and the recursive call passes the simple name of the nested class + // for the "nestedName(props)" part. + val prefix = "$name: $name" + "$prefix${buildKeyString(elementDescriptor, "")}" + } else { + name + } + } + val result = properties.mapIndexed { index, property -> + if (index < properties.size - 1) "$property," else property + }.joinToString("\n") + result.removeSuffix(",") + return result + } + } +} \ No newline at end of file diff --git a/Postgrest/src/commonTest/kotlin/RawQueryHelperTest.kt b/Postgrest/src/commonTest/kotlin/RawQueryHelperTest.kt new file mode 100644 index 000000000..7732926ad --- /dev/null +++ b/Postgrest/src/commonTest/kotlin/RawQueryHelperTest.kt @@ -0,0 +1,115 @@ +import io.github.jan.supabase.postgrest.query.RawQueryHelper +import kotlin.test.Test +import kotlin.test.assertEquals + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +class RawQueryHelperTest { + + @Test + fun queryWithMultipleForeignKeys_whenClassHasOtherClasses() { + val result = RawQueryHelper.queryWithMultipleForeignKeys() + assertEquals( + """ + content, + recipient: recipient(id, username, email), + sender: sender(id, username, email), + created_at + """.trimIndent(), result + ) + } + + @Test + fun queryWithMultipleForeignKeys_whenClassDoesNotHaveOtherClasses() { + val result = RawQueryHelper.queryWithMultipleForeignKeys() + assertEquals( + """ + userdto(id, username, email) + """.trimIndent(), result + ) + } + + @Test + fun queryWithMultipleForeignKeys_whenClassHasOneProperty() { + val result = RawQueryHelper.queryWithMultipleForeignKeys() + assertEquals( + """ + usersingleproperty(name) + """.trimIndent(), result + ) + } + + @Test + fun queryWithForeignKey() { + val result = RawQueryHelper.queryWithForeignKey() + println(result) + assertEquals( + """ + name, + address, + owner(name) + """.trimIndent(), result + ) + } + + + @Test + fun queryWithForeignKey_whenForeignKeyNameIsCustomized() { + val result = RawQueryHelper.queryWithForeignKey("custom") + println(result) + assertEquals( + """ + name, + address, + custom(name) + """.trimIndent(), result + ) + } +} + + +@Serializable +private data class MessageDto( + @SerialName("content") + val content: String, + + @SerialName("recipient") + val recipient: UserDto, + + @SerialName("sender") + val createdBy: UserDto, + + @SerialName("created_at") + val createdAt: String, +) + +@Serializable +private data class UserDto( + @SerialName("id") + val id: String, + + @SerialName("username") + val username: String, + + @SerialName("email") + val email: String, +) + +@Serializable +private data class UserSingleProperty( + @SerialName("name") + val id: String, +) + +@Serializable +private data class System( + @SerialName("name") + val name: String, + + @SerialName("address") + val address: String, + + @SerialName("owner") + val owner: UserSingleProperty, +) \ No newline at end of file