Skip to content

Introduce helpers to construct raw query string for FK data fetching with serializable class #965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<MessageDto>()
* // 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<MessageDto>()
* ```
*/
inline fun <reified T> queryWithMultipleForeignKeys(): String {
val lowercasedClassName = T::class.simpleName?.lowercase()
val descriptor: SerialDescriptor = serializer<T>().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<System>()
* // Returns:
* """
* name,
* owner: owner(id, username)
* """
*
* val customQuery = RawQueryHelper.queryWithForeignKey<System>("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<System>()
*```
*/
inline fun <reified T> queryWithForeignKey(customizedClassName: String? = null): String {
val descriptor: SerialDescriptor = serializer<T>().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 {

Check warning

Code scanning / detekt

Public functions require documentation. Warning

The function buildKeyString is missing documentation.
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
}
}
}
115 changes: 115 additions & 0 deletions Postgrest/src/commonTest/kotlin/RawQueryHelperTest.kt
Original file line number Diff line number Diff line change
@@ -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<MessageDto>()
assertEquals(
"""
content,
recipient: recipient(id, username, email),
sender: sender(id, username, email),
created_at
""".trimIndent(), result
)
}

@Test
fun queryWithMultipleForeignKeys_whenClassDoesNotHaveOtherClasses() {
val result = RawQueryHelper.queryWithMultipleForeignKeys<UserDto>()
assertEquals(
"""
userdto(id, username, email)
""".trimIndent(), result
)
}

@Test
fun queryWithMultipleForeignKeys_whenClassHasOneProperty() {
val result = RawQueryHelper.queryWithMultipleForeignKeys<UserSingleProperty>()
assertEquals(
"""
usersingleproperty(name)
""".trimIndent(), result
)
}

@Test
fun queryWithForeignKey() {
val result = RawQueryHelper.queryWithForeignKey<System>()
println(result)
assertEquals(
"""
name,
address,
owner(name)
""".trimIndent(), result
)
}


@Test
fun queryWithForeignKey_whenForeignKeyNameIsCustomized() {
val result = RawQueryHelper.queryWithForeignKey<System>("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,
)
Loading