Skip to content

Commit 69bf551

Browse files
Merge pull request #55 from DominikZajac/live-query-listeners
feat: add live query listeners and fix dates conversion
2 parents fe86eae + f4d9445 commit 69bf551

File tree

4 files changed

+136
-28
lines changed

4 files changed

+136
-28
lines changed

android/src/main/java/com/cblreactnative/CblReactnativeModule.kt

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class CblReactnativeModule(reactContext: ReactApplicationContext) :
2929

3030
// Property to hold the context
3131
private val context: ReactApplicationContext = reactContext
32+
private val queryChangeListeners: MutableMap<String, ListenerToken> = mutableMapOf()
3233
private val replicatorChangeListeners: MutableMap<String, ListenerToken> = mutableMapOf()
3334
private val replicatorDocumentListeners: MutableMap<String, ListenerToken> = mutableMapOf()
3435
private val collectionChangeListeners: MutableMap<String, ListenerToken> = mutableMapOf()
@@ -1047,6 +1048,88 @@ fun collection_AddDocumentChangeListener(
10471048
}
10481049
}
10491050

1051+
@ReactMethod
1052+
fun query_AddChangeListener(
1053+
changeListenerToken: String,
1054+
query: String,
1055+
parameters: ReadableMap?,
1056+
name: String,
1057+
promise: Promise
1058+
) {
1059+
GlobalScope.launch(Dispatchers.IO) {
1060+
try {
1061+
if (!DataValidation.validateDatabaseName(name, promise) || !DataValidation.validateQuery(query, promise)) {
1062+
return@launch
1063+
}
1064+
val database = DatabaseManager.getDatabase(name)
1065+
if (database == null) {
1066+
context.runOnUiQueueThread {
1067+
promise.reject("DATABASE_ERROR", "Could not find database with name $name")
1068+
}
1069+
return@launch
1070+
}
1071+
val queryObj = database.createQuery(query)
1072+
if (parameters != null && parameters.keySetIterator().hasNextKey()) {
1073+
val params = DataAdapter.readableMapToParameters(parameters)
1074+
queryObj.parameters = params
1075+
}
1076+
val listener = queryObj.addChangeListener { change ->
1077+
val resultMap = Arguments.createMap()
1078+
resultMap.putString("token", changeListenerToken)
1079+
change.results?.let { results ->
1080+
val resultList = mutableListOf<String>()
1081+
for (result in results) {
1082+
resultList.add(result.toJSON())
1083+
}
1084+
val jsonArray = "[" + resultList.joinToString(",") + "]"
1085+
resultMap.putString("data", jsonArray)
1086+
}
1087+
change.error?.let { error ->
1088+
resultMap.putString("error", error.localizedMessage)
1089+
}
1090+
context.runOnUiQueueThread {
1091+
sendEvent(context, "queryChange", resultMap)
1092+
}
1093+
}
1094+
queryChangeListeners[changeListenerToken] = listener
1095+
context.runOnUiQueueThread {
1096+
promise.resolve(null)
1097+
}
1098+
} catch (e: Throwable) {
1099+
context.runOnUiQueueThread {
1100+
promise.reject("QUERY_ERROR", e.message)
1101+
}
1102+
}
1103+
}
1104+
}
1105+
1106+
@ReactMethod
1107+
fun query_RemoveChangeListener(
1108+
changeListenerToken: String,
1109+
promise: Promise
1110+
) {
1111+
GlobalScope.launch(Dispatchers.IO) {
1112+
try {
1113+
if (queryChangeListeners.containsKey(changeListenerToken)) {
1114+
val listener = queryChangeListeners[changeListenerToken]
1115+
listener?.remove()
1116+
queryChangeListeners.remove(changeListenerToken)
1117+
context.runOnUiQueueThread {
1118+
promise.resolve(null)
1119+
}
1120+
} else {
1121+
context.runOnUiQueueThread {
1122+
promise.reject("QUERY_ERROR", "No query listener found for token $changeListenerToken")
1123+
}
1124+
}
1125+
} catch (e: Throwable) {
1126+
context.runOnUiQueueThread {
1127+
promise.reject("QUERY_ERROR", e.message)
1128+
}
1129+
}
1130+
}
1131+
}
1132+
10501133
// Replicator Functions
10511134
@ReactMethod
10521135
fun replicator_AddChangeListener(

android/src/main/java/com/cblreactnative/DataAdapter.kt

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import java.net.URI
1414
import java.text.SimpleDateFormat
1515
import java.util.Date
1616
import java.util.Locale
17+
import java.util.TimeZone
1718
import com.couchbase.lite.Collection as CBLCollection
1819

1920
object DataAdapter {
@@ -336,36 +337,43 @@ object DataAdapter {
336337
val iterator = map.keySetIterator()
337338
var count = 0
338339
while (iterator.hasNextKey()) {
339-
val key = iterator.nextKey()
340-
val nestedMap = map.getMap(key)
341-
val nestedType = nestedMap?.getString("type")
342-
count += 1
343-
when (nestedType) {
344-
"int" -> queryParameters.setInt(key, nestedMap.getDouble("value").toInt())
345-
"long" -> queryParameters.setLong(key, nestedMap.getDouble("value").toLong())
346-
"float" -> queryParameters.setFloat(key, nestedMap.getDouble("value").toFloat())
347-
"double" -> queryParameters.setDouble(key, nestedMap.getDouble("value"))
348-
"boolean" -> queryParameters.setBoolean(key, nestedMap.getBoolean("value"))
349-
"string" -> queryParameters.setString(key, nestedMap.getString("value"))
350-
"date" -> {
351-
val stringValue = map.getString("value")
352-
stringValue?.let { strValue ->
353-
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
354-
val date = dateFormat.parse(strValue)
355-
date?.let { d ->
356-
queryParameters.setDate(key, d)
340+
val key = iterator.nextKey()
341+
val nestedMap = map.getMap(key)
342+
val nestedType = nestedMap?.getString("type")
343+
count += 1
344+
when (nestedType) {
345+
"int" -> queryParameters.setInt(key, nestedMap.getDouble("value").toInt())
346+
"long" -> queryParameters.setLong(key, nestedMap.getDouble("value").toLong())
347+
"float" -> queryParameters.setFloat(key, nestedMap.getDouble("value").toFloat())
348+
"double" -> queryParameters.setDouble(key, nestedMap.getDouble("value"))
349+
"boolean" -> queryParameters.setBoolean(key, nestedMap.getBoolean("value"))
350+
"string" -> queryParameters.setString(key, nestedMap.getString("value"))
351+
"date" -> {
352+
val stringValue = nestedMap.getString("value")
353+
stringValue?.let { strValue ->
354+
val date = parseIsoDate(strValue)
355+
date?.let { d ->
356+
queryParameters.setDate(key, d)
357+
}
358+
}
357359
}
358-
}
360+
"value" -> {
361+
val value = nestedMap.getDynamic("value")
362+
when (value.type) {
363+
ReadableType.Boolean -> queryParameters.setBoolean(key, value.asBoolean())
364+
ReadableType.Number -> queryParameters.setDouble(key, value.asDouble())
365+
ReadableType.String -> queryParameters.setString(key, value.asString())
366+
else -> queryParameters.setValue(key, value)
367+
}
368+
}
369+
else -> throw Exception("Error: Invalid parameter type: $nestedType")
359370
}
360-
361-
else -> throw Exception("Error: Invalid parameter type")
362-
}
363371
}
364372
if (count == 0) {
365-
return null
373+
return null
366374
}
367375
return queryParameters
368-
}
376+
}
369377

370378
/**
371379
* Converts a `ReadableMap` to a `ReplicatorConfiguration` object.
@@ -559,4 +567,21 @@ object DataAdapter {
559567
}
560568
return map
561569
}
570+
571+
private fun parseIsoDate(dateString: String): Date? {
572+
val formats = listOf(
573+
"yyyy-MM-dd'T'HH:mm:ss.SSSX", // Handles Z or +hh:mm
574+
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // Handles Z
575+
"yyyy-MM-dd'T'HH:mm:ssX", // No milliseconds, with zone
576+
"yyyy-MM-dd'T'HH:mm:ss" // No milliseconds, no zone
577+
)
578+
for (format in formats) {
579+
try {
580+
val sdf = SimpleDateFormat(format, Locale.getDefault())
581+
sdf.timeZone = TimeZone.getTimeZone("UTC")
582+
return sdf.parse(dateString)
583+
} catch (_: Exception) { }
584+
}
585+
return null
586+
}
562587
}

expo-example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
PODS:
22
- boost (1.84.0)
3-
- cbl-reactnative (0.2.3):
3+
- cbl-reactnative (0.5.0):
44
- CouchbaseLite-Swift-Enterprise (= 3.2.1)
55
- DoubleConversion
66
- glog
@@ -2185,7 +2185,7 @@ EXTERNAL SOURCES:
21852185

21862186
SPEC CHECKSUMS:
21872187
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
2188-
cbl-reactnative: aec81403856ac5f91d3792b18da6a67551402f76
2188+
cbl-reactnative: f3508d36c0f5c6846d7e3f77175a603a4ea15b2c
21892189
CouchbaseLite-Swift-Enterprise: 6a1eddeed0b450d00d2336bcf60c9a71e228f0e4
21902190
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
21912191
EXConstants: 277129d9a42ba2cf1fad375e7eaa9939005c60be

expo-example/ios/expoexample.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@
367367
);
368368
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
369369
PRODUCT_BUNDLE_IDENTIFIER = "com.couchbase.rn.expo-example";
370-
PRODUCT_NAME = expoexample;
370+
PRODUCT_NAME = "expoexample";
371371
SWIFT_OBJC_BRIDGING_HEADER = "expoexample/expoexample-Bridging-Header.h";
372372
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
373373
SWIFT_VERSION = 5.0;
@@ -395,7 +395,7 @@
395395
);
396396
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
397397
PRODUCT_BUNDLE_IDENTIFIER = "com.couchbase.rn.expo-example";
398-
PRODUCT_NAME = expoexample;
398+
PRODUCT_NAME = "expoexample";
399399
SWIFT_OBJC_BRIDGING_HEADER = "expoexample/expoexample-Bridging-Header.h";
400400
SWIFT_VERSION = 5.0;
401401
TARGETED_DEVICE_FAMILY = "1,2";

0 commit comments

Comments
 (0)