
iosMain.dev.gitlive.firebase.firestore.firestore.kt Maven / Gradle / Ivy
/*
* Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
*/
package dev.gitlive.firebase.firestore
import cocoapods.FirebaseFirestore.*
import cocoapods.FirebaseFirestore.FIRDocumentChangeType.*
import dev.gitlive.firebase.*
import kotlinx.cinterop.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import platform.Foundation.NSError
import platform.Foundation.NSNull
@PublishedApi
internal inline fun decode(value: Any?): T =
decode(value) { (it as? FIRTimestamp)?.run { seconds * 1000 + (nanoseconds / 1000000.0) } }
internal fun decode(strategy: DeserializationStrategy, value: Any?): T =
decode(strategy, value) { (it as? FIRTimestamp)?.run { seconds * 1000 + (nanoseconds / 1000000.0) } }
@PublishedApi
internal inline fun encode(value: T, shouldEncodeElementDefault: Boolean) =
encode(value, shouldEncodeElementDefault, FIRFieldValue.fieldValueForServerTimestamp())
private fun encode(strategy: SerializationStrategy , value: T, shouldEncodeElementDefault: Boolean): Any? =
encode(strategy, value, shouldEncodeElementDefault, FIRFieldValue.fieldValueForServerTimestamp())
actual val Firebase.firestore get() =
FirebaseFirestore(FIRFirestore.firestore())
actual fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore {
return FirebaseFirestore(FIRFirestore.firestoreForApp(app.ios))
}
@Suppress("UNCHECKED_CAST")
actual class FirebaseFirestore(val ios: FIRFirestore) {
actual fun collection(collectionPath: String) = CollectionReference(ios.collectionWithPath(collectionPath))
actual fun collectionGroup(collectionId: String) = Query(ios.collectionGroupWithID(collectionId))
actual fun document(documentPath: String) = DocumentReference(ios.documentWithPath(documentPath))
actual fun batch() = WriteBatch(ios.batch())
actual fun setLoggingEnabled(loggingEnabled: Boolean): Unit =
FIRFirestore.enableLogging(loggingEnabled)
actual suspend fun runTransaction(func: suspend Transaction.() -> T) =
awaitResult { ios.runTransactionWithBlock({ transaction, _ -> runBlocking { Transaction(transaction!!).func() } }, it) } as T
actual suspend fun clearPersistence() =
await { ios.clearPersistenceWithCompletion(it) }
actual fun useEmulator(host: String, port: Int) {
ios.settings = ios.settings.apply {
this.host = "$host:$port"
persistenceEnabled = false
sslEnabled = false
}
}
actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) {
ios.settings = FIRFirestoreSettings().also { settings ->
persistenceEnabled?.let { settings.persistenceEnabled = it }
sslEnabled?.let { settings.sslEnabled = it }
host?.let { settings.host = it }
cacheSizeBytes?.let { settings.cacheSizeBytes = it }
}
}
actual suspend fun disableNetwork() {
await { ios.disableNetworkWithCompletion(it) }
}
actual suspend fun enableNetwork() {
await { ios.enableNetworkWithCompletion(it) }
}
}
@Suppress("UNCHECKED_CAST")
actual class WriteBatch(val ios: FIRWriteBatch) {
actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean) =
ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this }
actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) =
ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this }
actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) =
ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this }
actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) =
ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this }
actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) =
ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this }
actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) =
ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this }
actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) =
ios.updateData(encode(data, encodeDefaults) as Map, documentRef.ios).let { this }
actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) =
ios.updateData(encode(strategy, data, encodeDefaults) as Map, documentRef.ios).let { this }
actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) =
ios.updateData(fieldsAndValues.associate { it }, documentRef.ios).let { this }
actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) =
ios.updateData(fieldsAndValues.associate { (path, value) -> path.ios to value }, documentRef.ios).let { this }
actual fun delete(documentRef: DocumentReference) =
ios.deleteDocument(documentRef.ios).let { this }
actual suspend fun commit() = await { ios.commitWithCompletion(it) }
}
@Suppress("UNCHECKED_CAST")
actual class Transaction(val ios: FIRTransaction) {
actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean) =
ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this }
actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) =
ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this }
actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) =
ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this }
actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) =
ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this }
actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) =
ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this }
actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) =
ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this }
actual fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) =
ios.updateData(encode(data, encodeDefaults) as Map, documentRef.ios).let { this }
actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) =
ios.updateData(encode(strategy, data, encodeDefaults) as Map, documentRef.ios).let { this }
actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) =
ios.updateData(fieldsAndValues.associate { it }, documentRef.ios).let { this }
actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) =
ios.updateData(fieldsAndValues.associate { (path, value) -> path.ios to value }, documentRef.ios).let { this }
actual fun delete(documentRef: DocumentReference) =
ios.deleteDocument(documentRef.ios).let { this }
actual suspend fun get(documentRef: DocumentReference) =
throwError { DocumentSnapshot(ios.getDocument(documentRef.ios, it)!!) }
}
@Suppress("UNCHECKED_CAST")
actual class DocumentReference(val ios: FIRDocumentReference) {
actual val id: String
get() = ios.documentID
actual val path: String
get() = ios.path
actual val parent: CollectionReference
get() = CollectionReference(ios.parent)
actual fun collection(collectionPath: String) = CollectionReference(ios.collectionWithPath(collectionPath))
actual suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean) =
await { ios.setData(encode(data, encodeDefaults)!! as Map, merge, it) }
actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) =
await { ios.setData(encode(data, encodeDefaults)!! as Map, mergeFields.asList(), it) }
actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) =
await { ios.setData(encode(data, encodeDefaults)!! as Map, mergeFieldPaths.map { it.ios }, it) }
actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) =
await { ios.setData(encode(strategy, data, encodeDefaults)!! as Map, merge, it) }
actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) =
await { ios.setData(encode(strategy, data, encodeDefaults)!! as Map, mergeFields.asList(), it) }
actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) =
await { ios.setData(encode(strategy, data, encodeDefaults)!! as Map, mergeFieldPaths.map { it.ios }, it) }
actual suspend inline fun update(data: T, encodeDefaults: Boolean) =
await { ios.updateData(encode(data, encodeDefaults) as Map, it) }
actual suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) =
await { ios.updateData(encode(strategy, data, encodeDefaults) as Map, it) }
actual suspend fun update(vararg fieldsAndValues: Pair) =
await { block -> ios.updateData(fieldsAndValues.associate { it }, block) }
actual suspend fun update(vararg fieldsAndValues: Pair) =
await { block -> ios.updateData(fieldsAndValues.associate { (path, value) -> path.ios to value }, block) }
actual suspend fun delete() =
await { ios.deleteDocumentWithCompletion(it) }
actual suspend fun get() =
DocumentSnapshot(awaitResult { ios.getDocumentWithCompletion(it) })
actual val snapshots get() = callbackFlow {
val listener = ios.addSnapshotListener { snapshot, error ->
snapshot?.let { trySend(DocumentSnapshot(snapshot)) }
error?.let { close(error.toException()) }
}
awaitClose { listener.remove() }
}
}
actual open class Query(open val ios: FIRQuery) {
actual suspend fun get() = QuerySnapshot(awaitResult { ios.getDocumentsWithCompletion(it) })
actual fun limit(limit: Number) = Query(ios.queryLimitedTo(limit.toLong()))
actual val snapshots get() = callbackFlow {
val listener = ios.addSnapshotListener { snapshot, error ->
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
error?.let { close(error.toException()) }
}
awaitClose { listener.remove() }
}
actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow {
val listener = ios.addSnapshotListenerWithIncludeMetadataChanges(includeMetadataChanges) { snapshot, error ->
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
error?.let { close(error.toException()) }
}
awaitClose { listener.remove() }
}
internal actual fun _where(field: String, equalTo: Any?) = Query(ios.queryWhereField(field, isEqualTo = equalTo!!))
internal actual fun _where(path: FieldPath, equalTo: Any?) = Query(ios.queryWhereFieldPath(path.ios, isEqualTo = equalTo!!))
internal actual fun _where(field: String, equalTo: DocumentReference) = Query(ios.queryWhereField(field, isEqualTo = equalTo.ios))
internal actual fun _where(path: FieldPath, equalTo: DocumentReference) = Query(ios.queryWhereFieldPath(path.ios, isEqualTo = equalTo.ios))
internal actual fun _where(field: String, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
(lessThan?.let { ios.queryWhereField(field, isLessThan = it) } ?: ios).let { ios2 ->
(greaterThan?.let { ios2.queryWhereField(field, isGreaterThan = it) } ?: ios2).let { ios3 ->
arrayContains?.let { ios3.queryWhereField(field, arrayContains = it) } ?: ios3
}
}
)
internal actual fun _where(path: FieldPath, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
(lessThan?.let { ios.queryWhereFieldPath(path.ios, isLessThan = it) } ?: ios).let { ios2 ->
(greaterThan?.let { ios2.queryWhereFieldPath(path.ios, isGreaterThan = it) } ?: ios2).let { ios3 ->
arrayContains?.let { ios3.queryWhereFieldPath(path.ios, arrayContains = it) } ?: ios3
}
}
)
internal actual fun _where(field: String, inArray: List?, arrayContainsAny: List?) = Query(
(inArray?.let { ios.queryWhereField(field, `in` = it) } ?: ios).let { ios2 ->
arrayContainsAny?.let { ios2.queryWhereField(field, arrayContainsAny = arrayContainsAny) } ?: ios2
}
)
internal actual fun _where(path: FieldPath, inArray: List?, arrayContainsAny: List?) = Query(
(inArray?.let { ios.queryWhereFieldPath(path.ios, `in` = it) } ?: ios).let { ios2 ->
arrayContainsAny?.let { ios2.queryWhereFieldPath(path.ios, arrayContainsAny = arrayContainsAny) } ?: ios2
}
)
internal actual fun _orderBy(field: String, direction: Direction) = Query(ios.queryOrderedByField(field, direction == Direction.DESCENDING))
internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(ios.queryOrderedByFieldPath(field.ios, direction == Direction.DESCENDING))
internal actual fun _startAfter(document: DocumentSnapshot) = Query(ios.queryStartingAfterDocument(document.ios))
internal actual fun _startAfter(vararg fieldValues: Any) = Query(ios.queryStartingAfterValues(fieldValues.asList()))
internal actual fun _startAt(document: DocumentSnapshot) = Query(ios.queryStartingAtDocument(document.ios))
internal actual fun _startAt(vararg fieldValues: Any) = Query(ios.queryStartingAtValues(fieldValues.asList()))
internal actual fun _endBefore(document: DocumentSnapshot) = Query(ios.queryEndingBeforeDocument(document.ios))
internal actual fun _endBefore(vararg fieldValues: Any) = Query(ios.queryEndingBeforeValues(fieldValues.asList()))
internal actual fun _endAt(document: DocumentSnapshot) = Query(ios.queryEndingAtDocument(document.ios))
internal actual fun _endAt(vararg fieldValues: Any) = Query(ios.queryEndingAtValues(fieldValues.asList()))
}
@Suppress("UNCHECKED_CAST")
actual class CollectionReference(override val ios: FIRCollectionReference) : Query(ios) {
actual val path: String
get() = ios.path
actual val document get() = DocumentReference(ios.documentWithAutoID())
actual val parent get() = ios.parent?.let{DocumentReference(it)}
actual fun document(documentPath: String) = DocumentReference(ios.documentWithPath(documentPath))
actual suspend inline fun add(data: T, encodeDefaults: Boolean) =
DocumentReference(await { ios.addDocumentWithData(encode(data, encodeDefaults) as Map, it) })
actual suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean) =
DocumentReference(await { ios.addDocumentWithData(encode(strategy, data, encodeDefaults) as Map, it) })
actual suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) =
DocumentReference(await { ios.addDocumentWithData(encode(strategy, data, encodeDefaults) as Map, it) })
}
actual class FirebaseFirestoreException(message: String, val code: FirestoreExceptionCode) : FirebaseException(message)
actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code
actual enum class FirestoreExceptionCode {
OK,
CANCELLED,
UNKNOWN,
INVALID_ARGUMENT,
DEADLINE_EXCEEDED,
NOT_FOUND,
ALREADY_EXISTS,
PERMISSION_DENIED,
RESOURCE_EXHAUSTED,
FAILED_PRECONDITION,
ABORTED,
OUT_OF_RANGE,
UNIMPLEMENTED,
INTERNAL,
UNAVAILABLE,
DATA_LOSS,
UNAUTHENTICATED
}
actual enum class Direction {
ASCENDING,
DESCENDING
}
actual enum class ChangeType(internal val ios: FIRDocumentChangeType) {
ADDED(FIRDocumentChangeTypeAdded),
MODIFIED(FIRDocumentChangeTypeModified),
REMOVED(FIRDocumentChangeTypeRemoved)
}
fun NSError.toException() = when(domain) {
FIRFirestoreErrorDomain -> when(code) {
FIRFirestoreErrorCodeOK -> FirestoreExceptionCode.OK
FIRFirestoreErrorCodeCancelled -> FirestoreExceptionCode.CANCELLED
FIRFirestoreErrorCodeUnknown -> FirestoreExceptionCode.UNKNOWN
FIRFirestoreErrorCodeInvalidArgument -> FirestoreExceptionCode.INVALID_ARGUMENT
FIRFirestoreErrorCodeDeadlineExceeded -> FirestoreExceptionCode.DEADLINE_EXCEEDED
FIRFirestoreErrorCodeNotFound -> FirestoreExceptionCode.NOT_FOUND
FIRFirestoreErrorCodeAlreadyExists -> FirestoreExceptionCode.ALREADY_EXISTS
FIRFirestoreErrorCodePermissionDenied -> FirestoreExceptionCode.PERMISSION_DENIED
FIRFirestoreErrorCodeResourceExhausted -> FirestoreExceptionCode.RESOURCE_EXHAUSTED
FIRFirestoreErrorCodeFailedPrecondition -> FirestoreExceptionCode.FAILED_PRECONDITION
FIRFirestoreErrorCodeAborted -> FirestoreExceptionCode.ABORTED
FIRFirestoreErrorCodeOutOfRange -> FirestoreExceptionCode.OUT_OF_RANGE
FIRFirestoreErrorCodeUnimplemented -> FirestoreExceptionCode.UNIMPLEMENTED
FIRFirestoreErrorCodeInternal -> FirestoreExceptionCode.INTERNAL
FIRFirestoreErrorCodeUnavailable -> FirestoreExceptionCode.UNAVAILABLE
FIRFirestoreErrorCodeDataLoss -> FirestoreExceptionCode.DATA_LOSS
FIRFirestoreErrorCodeUnauthenticated -> FirestoreExceptionCode.UNAUTHENTICATED
else -> FirestoreExceptionCode.UNKNOWN
}
else -> FirestoreExceptionCode.UNKNOWN
}.let { FirebaseFirestoreException(description!!, it) }
actual class QuerySnapshot(val ios: FIRQuerySnapshot) {
actual val documents
get() = ios.documents.map { DocumentSnapshot(it as FIRDocumentSnapshot) }
actual val documentChanges
get() = ios.documentChanges.map { DocumentChange(it as FIRDocumentChange) }
actual val metadata: SnapshotMetadata get() = SnapshotMetadata(ios.metadata)
}
actual class DocumentChange(val ios: FIRDocumentChange) {
actual val document: DocumentSnapshot
get() = DocumentSnapshot(ios.document)
actual val newIndex: Int
get() = ios.newIndex.toInt()
actual val oldIndex: Int
get() = ios.oldIndex.toInt()
actual val type: ChangeType
get() = ChangeType.values().first { it.ios == ios.type }
}
@Suppress("UNCHECKED_CAST")
actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) {
actual val id get() = ios.documentID
actual val reference get() = DocumentReference(ios.reference)
actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T {
val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
return decode(value = data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } })
}
actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T {
val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
return decode(strategy, data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } })
}
actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T {
val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull }
return decode(value)
}
actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T {
val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull }
return decode(strategy, value)
}
actual fun contains(field: String) = ios.valueForField(field) != null
actual val exists get() = ios.exists
actual val metadata: SnapshotMetadata get() = SnapshotMetadata(ios.metadata)
fun ServerTimestampBehavior.toIos() : FIRServerTimestampBehavior = when (this) {
ServerTimestampBehavior.ESTIMATE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorEstimate
ServerTimestampBehavior.NONE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorNone
ServerTimestampBehavior.PREVIOUS -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorPrevious
}
}
actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) {
actual val hasPendingWrites: Boolean get() = ios.pendingWrites
actual val isFromCache: Boolean get() = ios.fromCache
}
actual class FieldPath private constructor(val ios: FIRFieldPath) {
actual constructor(vararg fieldNames: String) : this(FIRFieldPath(fieldNames.asList()))
actual val documentId: FieldPath get() = FieldPath(FIRFieldPath.documentID())
}
actual object FieldValue {
actual val serverTimestamp = Double.POSITIVE_INFINITY
actual val delete: Any get() = FIRFieldValue.fieldValueForDelete()
actual fun increment(value: Int): Any = FIRFieldValue.fieldValueForIntegerIncrement(value.toLong())
actual fun arrayUnion(vararg elements: Any): Any = FIRFieldValue.fieldValueForArrayUnion(elements.asList())
actual fun arrayRemove(vararg elements: Any): Any = FIRFieldValue.fieldValueForArrayRemove(elements.asList())
actual fun delete(): Any = delete
}
private fun T.throwError(block: T.(errorPointer: CPointer>) -> R): R {
memScoped {
val errorPointer: CPointer> = alloc>().ptr
val result = block(errorPointer)
val error: NSError? = errorPointer.pointed.value
if (error != null) {
throw error.toException()
}
return result
}
}
suspend inline fun awaitResult(function: (callback: (T?, NSError?) -> Unit) -> Unit): T {
val job = CompletableDeferred()
function { result, error ->
if(error == null) {
job.complete(result)
} else {
job.completeExceptionally(error.toException())
}
}
return job.await() as T
}
suspend inline fun await(function: (callback: (NSError?) -> Unit) -> T): T {
val job = CompletableDeferred()
val result = function { error ->
if(error == null) {
job.complete(Unit)
} else {
job.completeExceptionally(error.toException())
}
}
job.await()
return result
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy