Skip to content

Commit 291e56a

Browse files
committed
Adds Firestore extension functions for Kotlin
Introduces a set of extension functions for easier interaction with Google Cloud Firestore from Kotlin, including: - Upserting and creating documents - Retrieving documents and lists of documents - Batch writing and deleting documents efficiently - Deleting documents by field
1 parent 271ec01 commit 291e56a

2 files changed

Lines changed: 412 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package net.ghue.ktp.gcp.firestore
2+
3+
import com.google.cloud.firestore.CollectionReference
4+
import com.google.cloud.firestore.FieldPath
5+
import com.google.cloud.firestore.Firestore
6+
import com.google.cloud.firestore.Query
7+
import com.google.cloud.firestore.SetOptions
8+
import kotlin.reflect.KProperty1
9+
import kotlin.reflect.full.memberProperties
10+
import net.ghue.ktp.gcp.await
11+
12+
const val FIRESTORE_BATCH_SIZE = 500
13+
14+
/** Inserts or updates a document using its 'id' property as the document ID. */
15+
suspend inline fun <reified T : Any> CollectionReference.upsert(
16+
document: T,
17+
setOptions: SetOptions = SetOptions.merge(),
18+
) {
19+
document(idFieldValue(document)).set(document.serialize(), setOptions).await()
20+
}
21+
22+
/** Creates a new document with an auto-generated ID, passing the ID to the builder function. */
23+
suspend fun <T : Any> CollectionReference.newDoc(documentBuilder: (String) -> T): T {
24+
val newDocRef = this.document()
25+
val doc = documentBuilder(newDocRef.id)
26+
newDocRef.set(doc.serialize()).await()
27+
return doc
28+
}
29+
30+
/** Executes the query and returns all matching documents as a list. */
31+
suspend inline fun <reified T : Any> Query.getList(): List<T> =
32+
get().await().documents.mapNotNull { it.deserialize<T>() }
33+
34+
/** Executes the query and returns the first matching document, or null if none found. */
35+
suspend inline fun <reified T : Any> Query.firstOrNull(): T? =
36+
get().await().documents.firstOrNull()?.deserialize()
37+
38+
/** Gets a document by ID, throwing NoSuchElementException if not found. */
39+
suspend inline fun <reified T : Any> CollectionReference.getOrThrow(documentId: String): T {
40+
return getOrNull(documentId)
41+
?: throw NoSuchElementException("${T::class.simpleName} with id $documentId not found")
42+
}
43+
44+
/** Gets a document by ID, returning null if not found. */
45+
suspend inline fun <reified T : Any> CollectionReference.getOrNull(documentId: String): T? {
46+
val doc = this.document(documentId).get().await()
47+
return doc.deserialize<T>()
48+
}
49+
50+
/** Extracts the value of the 'id' property from the given data object. */
51+
fun idFieldValue(item: Any): String {
52+
val idProperty =
53+
item::class.memberProperties.find { it.name == "id" }
54+
?: error("Property 'id' not found on ${item::class.simpleName}")
55+
56+
val idValue =
57+
idProperty.getter.call(item) ?: error("Property 'id' is null on ${item::class.simpleName}")
58+
59+
val stringValue =
60+
if (idValue::class.isValue) {
61+
// Handle value class by getting the single property from the primary constructor
62+
val property =
63+
idValue::class.memberProperties.firstOrNull()
64+
?: error("Value class ${idValue::class.simpleName} has no properties")
65+
val innerValue =
66+
property.getter.call(idValue)
67+
?: error("Property 'id' is null on ${item::class.simpleName}")
68+
innerValue.toString()
69+
} else {
70+
idValue.toString()
71+
}
72+
73+
require(stringValue.isNotEmpty()) { "Property 'id' is empty on ${item::class.simpleName}" }
74+
return stringValue
75+
}
76+
77+
/** Writes multiple documents in batches, respecting Firestore's 500 document limit per batch. */
78+
suspend fun Firestore.batchWrite(collection: CollectionReference, items: List<Any>) {
79+
items.chunked(FIRESTORE_BATCH_SIZE).forEach { chunk ->
80+
val batch = batch()
81+
chunk.forEach { item ->
82+
batch.set(collection.document(idFieldValue(item)), item.serialize(), SetOptions.merge())
83+
}
84+
batch.commit().await()
85+
}
86+
}
87+
88+
/** Deletes all documents in a collection using batched operations. */
89+
suspend fun Firestore.deleteCollection(collection: CollectionReference) {
90+
while (true) {
91+
val snapshot =
92+
collection.limit(FIRESTORE_BATCH_SIZE).select(FieldPath.documentId()).get().await()
93+
if (snapshot.documents.isEmpty()) break
94+
95+
val batch = batch()
96+
snapshot.documents.forEach { batch.delete(it.reference) }
97+
batch.commit().await()
98+
}
99+
}
100+
101+
/**
102+
* Deletes documents from the specified collection where the given property matches the provided
103+
* value.
104+
*/
105+
suspend fun <T> Firestore.deleteByField(
106+
collection: CollectionReference,
107+
property: KProperty1<*, T>,
108+
value: T,
109+
) {
110+
while (true) {
111+
val snapshot =
112+
collection
113+
.whereEqualTo(property.name, value)
114+
.limit(FIRESTORE_BATCH_SIZE)
115+
.select(FieldPath.documentId())
116+
.get()
117+
.await()
118+
119+
if (snapshot.documents.isEmpty()) break
120+
121+
val batch = batch()
122+
snapshot.documents.forEach { batch.delete(it.reference) }
123+
batch.commit().await()
124+
}
125+
}

0 commit comments

Comments
 (0)