diff --git a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt index 1b428b93..55a99c54 100644 --- a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt +++ b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt @@ -5,6 +5,7 @@ import com.powersync.db.Queries import com.powersync.db.crud.CrudBatch import com.powersync.db.crud.CrudTransaction import com.powersync.sync.SyncStatus +import com.powersync.utils.JsonParam /** * A PowerSync managed database. @@ -27,14 +28,37 @@ public interface PowerSyncDatabase : Queries { * * The connection is automatically re-opened if it fails for any reason. * + * Use @param [connector] to specify the [PowerSyncBackendConnector]. * Use @param [crudThrottleMs] to specify the time between CRUD operations. Defaults to 1000ms. * Use @param [retryDelayMs] to specify the delay between retries after failure. Defaults to 5000ms. + * Use @param [params] to specify sync parameters from the client. * + * Example usage: + * ``` + * val params = JsonParam.Map( + * mapOf( + * "name" to JsonParam.String("John Doe"), + * "age" to JsonParam.Number(30), + * "isStudent" to JsonParam.Boolean(false) + * ) + * ) + * + * connect( + * connector = connector, + * crudThrottleMs = 2000L, + * retryDelayMs = 10000L, + * params = params + * ) + * ``` * TODO: Internal Team - Status changes are reported on [statusStream]. */ - public suspend fun connect(connector: PowerSyncBackendConnector, crudThrottleMs: Long = 1000L, - retryDelayMs: Long = 5000L) + public suspend fun connect( + connector: PowerSyncBackendConnector, + crudThrottleMs: Long = 1000L, + retryDelayMs: Long = 5000L, + params: Map = emptyMap() + ) /** @@ -94,4 +118,4 @@ public interface PowerSyncDatabase : Queries { * To preserve data in local-only tables, set clearLocal to false. */ public suspend fun disconnectAndClear(clearLocal: Boolean = true) -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt index 0cddc036..2bdd9e86 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt @@ -20,8 +20,9 @@ import com.powersync.db.internal.PowerSyncTransaction import com.powersync.db.schema.Schema import com.powersync.sync.SyncStatus import com.powersync.sync.SyncStream +import com.powersync.utils.JsonParam import com.powersync.utils.JsonUtil -import com.powersync.utils.Strings.quoteIdentifier +import com.powersync.utils.toJsonObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -85,7 +86,12 @@ internal class PowerSyncDatabaseImpl( } @OptIn(FlowPreview::class) - override suspend fun connect(connector: PowerSyncBackendConnector, crudThrottleMs: Long, retryDelayMs: Long) { + override suspend fun connect( + connector: PowerSyncBackendConnector, + crudThrottleMs: Long, + retryDelayMs: Long, + params: Map) + { // close connection if one is open disconnect() @@ -95,7 +101,8 @@ internal class PowerSyncDatabaseImpl( connector = connector, uploadCrud = suspend { connector.uploadData(this) }, retryDelayMs = retryDelayMs, - logger = logger + logger = logger, + params = params.toJsonObject() ) syncJob = scope.launch { diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt index 92eb7422..6b0c18c9 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt @@ -8,7 +8,9 @@ import com.powersync.bucket.Checkpoint import com.powersync.bucket.WriteCheckpointResponse import co.touchlab.stately.concurrency.AtomicBoolean import com.powersync.connectors.PowerSyncBackendConnector +import com.powersync.utils.JsonParam import com.powersync.utils.JsonUtil +import com.powersync.utils.toJsonObject import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.HttpTimeout @@ -41,7 +43,8 @@ internal class SyncStream( private val connector: PowerSyncBackendConnector, private val uploadCrud: suspend () -> Unit, private val retryDelayMs: Long = 5000L, - private val logger: Logger + private val logger: Logger, + private val params: JsonObject ) { private var isUploadingCrud = AtomicBoolean(false) @@ -245,7 +248,8 @@ internal class SyncStream( val req = StreamingSyncRequest( buckets = initialBuckets.map { (bucket, after) -> BucketRequest(bucket, after) }, - clientId = clientId!! + clientId = clientId!!, + parameters = params ) streamingSyncRequest(req).collect { value -> diff --git a/core/src/commonMain/kotlin/com/powersync/utils/Json.kt b/core/src/commonMain/kotlin/com/powersync/utils/Json.kt index 71fb9e64..d8ddac8f 100644 --- a/core/src/commonMain/kotlin/com/powersync/utils/Json.kt +++ b/core/src/commonMain/kotlin/com/powersync/utils/Json.kt @@ -1,6 +1,11 @@ package com.powersync.utils import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive /** * A global instance of a JSON serializer. @@ -12,3 +17,28 @@ internal object JsonUtil { } } +public sealed class JsonParam { + public data class Number(val value: kotlin.Number) : JsonParam() + public data class String(val value: kotlin.String) : JsonParam() + public data class Boolean(val value: kotlin.Boolean) : JsonParam() + public data class Map(val value: kotlin.collections.Map) : JsonParam() + public data class Collection(val value: kotlin.collections.Collection) : JsonParam() + public data class JsonElement(val value: kotlinx.serialization.json.JsonElement) : JsonParam() + public data object Null : JsonParam() + + internal fun toJsonElement(): kotlinx.serialization.json.JsonElement = when (this) { + is Number -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is Map -> JsonObject(value.mapValues { it.value.toJsonElement() }) + is Collection -> JsonArray(value.map { it.toJsonElement() }) + is JsonElement -> value + Null -> JsonNull + } +} + +public fun Map.toJsonObject(): JsonObject { + return JsonObject(this.mapValues { (_, value) -> + value?.toJsonElement() ?: JsonNull + }) +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt b/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt new file mode 100644 index 00000000..af3fd451 --- /dev/null +++ b/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt @@ -0,0 +1,167 @@ +package com.powersync.utils + + +import kotlinx.serialization.json.* +import kotlin.test.* + +class JsonTest { + @Test + fun testNumberToJsonElement() { + val number = JsonParam.Number(42) + val jsonElement = number.toJsonElement() + assertTrue(jsonElement is JsonPrimitive) + assertEquals(42, jsonElement.int) + } + + @Test + fun testStringToJsonElement() { + val string = JsonParam.String("test") + val jsonElement = string.toJsonElement() + assertTrue(jsonElement is JsonPrimitive) + assertEquals("test", jsonElement.content) + } + + @Test + fun testBooleanToJsonElement() { + val boolean = JsonParam.Boolean(true) + val jsonElement = boolean.toJsonElement() + assertTrue(jsonElement is JsonPrimitive) + assertTrue(jsonElement.boolean) + } + + @Test + fun testMapToJsonElement() { + val map = JsonParam.Map(mapOf( + "key1" to JsonParam.String("value1"), + "key2" to JsonParam.Number(42) + )) + val jsonElement = map.toJsonElement() + assertTrue(jsonElement is JsonObject) + assertEquals("value1", jsonElement["key1"]?.jsonPrimitive?.content) + assertEquals(42, jsonElement["key2"]?.jsonPrimitive?.int) + } + + @Test + fun testListToJsonElement() { + val list = JsonParam.Collection(listOf( + JsonParam.String("item1"), + JsonParam.Number(42) + )) + val jsonElement = list.toJsonElement() + assertTrue(jsonElement is JsonArray) + assertEquals("item1", jsonElement[0].jsonPrimitive.content) + assertEquals(42, jsonElement[1].jsonPrimitive.int) + } + + @Test + fun testJsonElementParamToJsonElement() { + val originalJson = buildJsonObject { + put("key", "value") + } + val jsonElementParam = JsonParam.JsonElement(originalJson) + val jsonElement = jsonElementParam.toJsonElement() + assertEquals(originalJson, jsonElement) + } + + @Test + fun testNullToJsonElement() { + val nullParam = JsonParam.Null + val jsonElement = nullParam.toJsonElement() + assertTrue(jsonElement is JsonNull) + } + + @Test + fun testMapToJsonObject() { + val params = mapOf( + "string" to JsonParam.String("value"), + "number" to JsonParam.Number(42), + "boolean" to JsonParam.Boolean(true), + "null" to JsonParam.Null + ) + val jsonObject = params.toJsonObject() + assertEquals("value", jsonObject["string"]?.jsonPrimitive?.content) + assertEquals(42, jsonObject["number"]?.jsonPrimitive?.int) + assertEquals(true, jsonObject["boolean"]?.jsonPrimitive?.boolean) + assertTrue(jsonObject["null"] is JsonNull) + } + + @Test + fun testComplexNestedMapToJsonObject() { + val complexNestedMap = mapOf( + "string" to JsonParam.String("value"), + "number" to JsonParam.Number(42), + "boolean" to JsonParam.Boolean(true), + "null" to JsonParam.Null, + "nestedMap" to JsonParam.Map(mapOf( + "list" to JsonParam.Collection(listOf( + JsonParam.Number(1), + JsonParam.String("two"), + JsonParam.Boolean(false) + )), + "deeplyNested" to JsonParam.Map(mapOf( + "jsonElement" to JsonParam.JsonElement(buildJsonObject { + put("key", "value") + put("array", buildJsonArray { + add(1) + add("string") + add(true) + }) + }), + "mixedList" to JsonParam.Collection(arrayListOf( + JsonParam.Number(3.14), + JsonParam.Map(mapOf( + "key" to JsonParam.String("nestedValue") + )), + JsonParam.Null + ) + ) + )) + )) + ) + + val jsonObject = complexNestedMap.toJsonObject() + + // Verify top-level elements + assertEquals("value", jsonObject["string"]?.jsonPrimitive?.content) + assertEquals(42, jsonObject["number"]?.jsonPrimitive?.int) + assertEquals(true, jsonObject["boolean"]?.jsonPrimitive?.boolean) + assertTrue(jsonObject["null"] is JsonNull) + + // Verify nested map + val nestedMap = jsonObject["nestedMap"]?.jsonObject + assertNotNull(nestedMap) + + // Verify nested list + val nestedList = nestedMap["list"]?.jsonArray + assertNotNull(nestedList) + assertEquals(3, nestedList.size) + assertEquals(1, nestedList[0].jsonPrimitive.int) + assertEquals("two", nestedList[1].jsonPrimitive.content) + assertEquals(false, nestedList[2].jsonPrimitive.boolean) + + // Verify deeply nested map + val deeplyNested = nestedMap["deeplyNested"]?.jsonObject + assertNotNull(deeplyNested) + + // Verify JsonElement + val jsonElement = deeplyNested["jsonElement"]?.jsonObject + assertNotNull(jsonElement) + assertEquals("value", jsonElement["key"]?.jsonPrimitive?.content) + val jsonElementArray = jsonElement["array"]?.jsonArray + assertNotNull(jsonElementArray) + assertEquals(3, jsonElementArray.size) + assertEquals(1, jsonElementArray[0].jsonPrimitive.int) + assertEquals("string", jsonElementArray[1].jsonPrimitive.content) + assertEquals(true, jsonElementArray[2].jsonPrimitive.boolean) + + // Verify mixed list + val mixedList = deeplyNested["mixedList"]?.jsonArray + assertNotNull(mixedList) + assertEquals(3, mixedList.size) + assertEquals(3.14, mixedList[0].jsonPrimitive.double) + val nestedMapInList = mixedList[1].jsonObject + assertNotNull(nestedMapInList) + assertEquals("nestedValue", nestedMapInList["key"]?.jsonPrimitive?.content) + assertTrue(mixedList[2] is JsonNull) + } +} \ No newline at end of file