Firestore Client for Kotlin JVM with strict (and relaxed) type-system.
Inspired by Kotlin Path API, where div /
symbol is overloaded to express file path.
See project website at firestore4k.io for detailed documentation & examples.
DSL to express Firestore collection & document path
val users = rootCollection<User, UserId>("users")
val messages = users.subCollection<Message, MessageId>("messages")
// /users
users
// /users/user1
users / UserId("user1")
// /users/user1/message
users / UserId("user1") / messages
// /users/user1/message/message1
users / UserId("user1") / messages / MessageId("message1")
Use collection & document path for operations.
val firestoreClient = FirestoreClient()
// add (ID auto generated by Firestore)
val userId: String = firestoreClient.add(users, User())
// set
firestoreClient.put(users / UserId("user1"), User())
// get
val user = firestoreClient.get<User>(users / UserId("user1"))
// get all
val messages = firestoreClient.getAll<Message>(users / UserId("user1") / messages)
Define flexible
dynamic or strict + type-inference
typed collection hierarchy.
// Using `dynamic` API
val users = collection("users")
val messages = collection("messages")
// OR
// Using `typed` API
val users = rootCollection<User, UserId>("users")
val messages = users.subCollection<Message, MessageId>("messages")
GCP Firestore client for Kotlin + Gradle project.
Firestore is a NoSQL document-store (tree based) database-as-a-service from Google Cloud Platform.
API in two flavors:
-
Dynamic → Flexible dynamic API with relaxed type checks for DB schema.
-
Typed → Typed-API with type safety for DB schema.
For Typed API, you can optionally use annotations along with KSP (Kotlin Symbol Processing) to autogenerate some boilerplate code.
Firestore stores the DB in alternate hierarchy of collections and documents.
Ref: https://firebase.google.com/docs/firestore/manage-data/structure-data
This structure is a mirror of the Resource Oriented Design of REST API Design guidelines recommended by Google.
Ref: https://cloud.google.com/apis/design/resources
-
Collections and documents are alternative in hierarchy: <collection>/<document>/<collection>/<document>
-
Top-level is always a collection, not a document.
-
Collection names as plural.
For the sample code, I will use a root (top-level) collection: users and its sub (child) collection: messages.
Path | Description |
---|---|
users |
users as root collection |
users/user1 |
user1 document under users root collection |
users/user1/messages |
messages sub-collection under user1 document |
users/user1/messages/message1 |
message1 document under messages sub-collection under user1 document |
Define collections
val users = collection("users")
val messages = collection("messages")
And then use them in PATHs of CRUD operations such as…
val firestoreClient = FirestoreClient()
// add (ID auto generated by Firestore)
val userId: String = firestoreClient.add(users, User())
val messageId: String = firestoreClient.add(users / "user1" / messages, Message())
// set
firestoreClient.put(users / "user1", User())
firestoreClient.put(users / "user1" / messages / "message1", Message())
// get
val user: User = firestoreClient.get(users / "user1")
val message: Message = firestoreClient.get(users / "user1" / messages / "message1")
// OR
val user = firestoreClient.get<User>(users / "user1")
val message = firestoreClient.get<Message>(users / "user1" / messages / "message1")
// get all
val users: Collection<User> = firestoreClient.getAll(users)
val messages: Collection<Message> = firestoreClient.getAll(users / "user1" / messages)
// OR
val users = firestoreClient.getAll<User>(users)
val messages = firestoreClient.getAll<Message>(users / "user1" / messages)
// delete
firestoreClient.delete(users / "user1" / messages / "message1")
firestoreClient.deleteAll(users / "user1" / messages)
firestoreClient.delete(users / "user1")
firestoreClient.deleteAll(users)
Define collection hierarchy and type bindings
val users = rootCollection<User>("users")
val messages = users.subCollection<User, Message>("messages")
CRUD operations for typed are similar to dynamic, but with type safety & inference.
-
So, users have to be root collection and messages under it.
-
Code accepts User / Message objects only in their respective add and put functions.
-
Type inference for return value of object & collection in get and getAll functions respectively.
val firestoreClient = FirestoreClient()
// add (ID auto generated by Firestore)
val userId: String = firestoreClient.add(users, User())
val messageId: String = firestoreClient.add(users / "user1" / messages, Message())
// set
firestoreClient.put(users / UserId("user1"), User())
firestoreClient.put(users / UserId("user1") / messages / MessageId("message1"), Message())
// get
val user = firestoreClient.get(users / UserId("user1"))
val message = firestoreClient.get(users / UserId("user1") / messages / MessageId("message1"))
// get all
val users = firestoreClient.getAll(users)
val messages = firestoreClient.getAll(users / UserId("user1") / messages)
// delete
firestoreClient.deleteDocument(users / UserId("user1") / messages / MessageId("message1"))
firestoreClient.deleteCollection(users / UserId("user1") / messages)
firestoreClient.deleteDocument(users / UserId("user1"))
firestoreClient.deleteCollection(users)
|
Experimental |
Collection hierarchy and type bindings are autogenerated using annotations.
But for simple cases, it is not worth the complexity since it is more verbose.
// root collection will not have @[ChildOf] annotation.
@Serializable
@Collection("users")
data class User(
val name: String,
)
@IdOf("users")
@JvmInline
value class UserId(private val value: String) {
override fun toString(): String = value
}
@Serializable
@Collection("messages")
@ChildOf("users")
data class Message(
val body: String,
)
@IdOf("messages")
@JvmInline
value class MessageId(private val value: String) {
override fun toString(): String = value
}
Add repository URL https://s01.oss.sonatype.org/content/repositories/snapshots/ for SNAPSHOT versions.
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
}
dependencies {
implementation("io.firestore4k:dynamic-api:$latestVersion")
}
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
}
dependencies {
implementation("io.firestore4k:typed-api:$latestVersion")
}
|
Experimental |
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
}
dependencies {
implementation("io.firestore4k:typed-api:$latestVersion")
compileOnly(project("io.firestore4k:annotations:$latestVersion"))
ksp(project("io.firestore4k:ksp:$latestVersion"))
}
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
sourceSets.test {
kotlin.srcDir("build/generated/ksp/test/kotlin")
}
}