In this part you will improve Kotlin-JavaScript inter-op by adding Kotlin headers (or bindings) for Firebase libraries. After that we will actually try to utilise some language features to make code a bit more idiomatic.
-
Create the file
Admin.kt
insrc/main/kotlin/bindings
. -
Write a basic JS binding to remove the
require
call and abstract away the module import:
@JsModule("firebase-admin")
@JsNonModule
external object Admin {
fun initializeApp(config: dynamic)
fun database(): dynamic
}
- In a similar way add another binding in
Functions.kt
:
@JsModule("firebase-functions")
@JsNonModule
external object Functions {
fun config(): dynamic
val https: dynamic
}
- Also rewrite some dynamic code in the main function with the newly written bindings:
fun main(args: Array<String>) {
Admin.initializeApp(Functions.config().firebase)
exports.testPush = Functions.https.onRequest { req, res ->
Admin.database().ref("/conversions")
.push(InputConversions(req.query.eur.toString().toFloat()))
.then { res.status(200).send("done") }
}
}
-
Build, deploy and make sure it still works.
-
Copy the rest of my custom bindings from the
bindings
folder of this repo tosrc/main/kotlin/bindings
. -
Optional: Examine copied bindings to see how they represents their respective JavaScript APIs.
-
Rewrite the database table types to Kotlin:
data class Rates(
val usd: Float,
val czk: Float,
val pln: Float,
val rub: Float
)
data class Conversions(
val eur: Float,
val usd: Float? = null,
val czk: Float? = null,
val pln: Float? = null,
val rub: Float? = null,
val timestamp: Long? = null
)
- Rewrite the function
convert
to Kotlin:
exports.convert = Functions.database.ref("/conversions/{conversionID}").onWrite<Conversions> { event ->
val input = event.data.`val`()
if (input.timestamp != null) return@onWrite
Admin.database().ref("/rates").once<Rates>("value")
.then<Conversions> { ratesSnap ->
ratesSnap.`val`().let {
Conversions(
eur = input.eur,
usd = input.eur * it.usd,
czk = input.eur * it.czk,
pln = input.eur * it.pln,
rub = input.eur * it.rub,
timestamp = Date.now()
)
}
}
.then { result ->
Admin.database().ref("/conversions")
.child(event.params.conversionID)
.set(result)
}
}
- Build and deploy to verify that it still works.
-
Move data classes to a separate package e.g.
domain
-
Add functions
inNew()
andconvert(Rates, Long)
toConversions.kt
:
fun Conversions.isNew() = timestamp == null
fun Conversions.convert(rates: Rates, timestamp: Long) = Conversions(
eur = eur,
usd = eur * rates.usd,
czk = eur * rates.czk,
pln = eur * rates.pln,
rub = eur * rates.rub,
timestamp = timestamp
)
- Make use of the new
Conversions
code:
fun main(args: Array<String>) {
Admin.initializeApp(Functions.config().firebase)
val database = Admin.database()
exports.testPush = Functions.https.onRequest { req, res ->
database.ref("/conversions")
.push(Conversions(req.query.eur.toString().toFloat()))
.then { res.status(200).send("done") }
}
exports.convert = Functions.database.ref("/conversions/{conversionID}").onWrite<Conversions> { event ->
event.data.`val`().takeIf { it.isNew() }?.let { input ->
database.ref("/rates").once<Rates>("value")
.then<Conversions> { it.`val`().let { rates -> input.convert(rates, Date.now()) } }
.then { conversions ->
database.ref("/conversions")
.child(event.params.conversionID)
.set(conversions)
}
}
}
}
Congratulations! You just completed this workshop and successfully created Firebase Functions project in Kotlin. I encourage you play and expand your project as much as you want (I do not encourage using this in production enviroment, do it at your own risk).
Check out optional extra part for some more info on generating Kotlin headers.