Database and Binary Storage Interface with adapter(s), written in Scala
This library serves two purposes:
- Provide a common abstraction for accessing and manipulating databases and binary storage backends
- Provide adapters for various databases and storage backends
I am a one-man show, so at best, what you see here is work I need in side projects. I've open-sourced this library because other people may find some of it useful.
This library is written in Scala. It might interoperate with other JVM languages, but I make no guarantees.
This library uses Typesafe's Play JSON library for serialization of content. I hope to support other mechanisms at some point.
In your build.sbt:
libraryDependencies += "com.seancheatham" %% "storage-firebase" % "0.1.3"
libraryDependencies += "com.seancheatham" %% "storage-google-cloud" % "0.1.3"
- Setup a Firebase Service Account
- Generate/Download a new Private Key
- Store the key as you see fit, depending on your environment setup. Just remember the path to it.
// You will need to provide an ExecutionContext. If you have one, use it, otherwise, you can use this:
import scala.concurrent.ExecutionContext.Implicits.global
import com.seancheatham.storage.firebase.FirebaseDatabase
val db: FirebaseDatabase =
FirebaseDatabase.fromServiceAccountKey("/path/to/key.json", "https://address-of-firebase-app.firebaseio.com")
Write or overwrite a value at a path. If data already existed at the path, it will be completely replaced.
import com.seancheatham.storage.DocumentStorage.DocumentStorage
import play.api.libs.json._
val db: DocumentStorage[JsValue] = ???
val value = JsString("Alan")
val userId = "1"
val writeFuture: Future[_] =
db.write("users", userId, "firstName")(value)
val userId = "1"
val readFuture: Future[String] =
db.get("users", userId, "firstName") // References /users/1/firstName
.map(_.as[String])
If the value doesn't exist, the Future will fail with a NoSuchElementException
Alternatively, if you know the value is generally optional, you can lift it instead.
val readOptionalFuture: Future[Option[JsValue]] =
db.lift("users", userId, "lastName")
Merges the given value into the value located at the given path. For example:
Original
{
"firstName": "Alan",
"email": "alan@turning.machine"
}
To Merge In
{
"email": "alan@saved.us",
"lastName": "Turing"
}
Results In
{
"firstName": "Alan",
"lastName": "Turing",
"email": "alan@saved.us"
}
val value = Json.obj("email" -> "alan@saved.us", "lastName" -> "Turing")
val userId = "1"
val mergeFuture: Future[_] =
db.merge("users", userId)(value)
val userId = "1"
val deleteFuture: Future[_] =
db.delete("users", userId, "lastName")
- Setup a Service Account
- Generate/Download a new Private Key
- Store the key as you see fit, depending on your environment setup. Just remember the path to it.
// You will need to provide an ExecutionContext. If you have one, use it, otherwise, you can use this:
import scala.concurrent.ExecutionContext.Implicits.global
import com.seancheatham.storage.gcloud.GoogleCloudStorage
val storage: GoogleCloudStorage =
GoogleCloudStorage("PROJECT_ID", "/path/to/key.json")
Write or overwrite the file at a key path. For bucket-based storage systems, like Google Cloud Storage, the first item in the key path represents the bucket.
val storage: BinaryStorage = ???
val bytes: Iterator[Byte] = ???
val future: Future[_] =
storage.write("BUCKET_NAME", "photos", "picture.jpg")(bytes)
val readFuture: Future[Iterator[Byte]] =
storage.get("BUCKET_NAME", "photos", "picture.jpg")
If the file doesn't exist, the Future will fail with a NoSuchElementException
Alternatively, if you know the value is generally optional, you can lift it instead.
val readOptionalFuture: Future[Option[Iterator[Byte]]] =
storage.lift("BUCKET_NAME", "photos", "picture.jpg")
val deleteFuture: Future[_] =
storage.delete("BUCKET_NAME", "photos", "picture.jpg")
Firebase provides functionality to attach listeners at key-paths in their realtime database. This allows your application to react to changes in data almost immediately after they occur. Firebase allows for listening to a value at a specific path, or for listening to children-changes at a specific path.
Listen for changes or deletions of a value at a given path.
import com.seancheatham.storage.firebase._
val db: FirebaseDatabase = ???
val watcherId =
db.watchValue("users", "1", "firstName")(
// Provide as many handlers as you want
// For example, if the value changes to a new value:
ValueChangedHandler(
(v: JsValue) =>
println(s"Hello ${v.as[String]}. It looks like you just changed your name!")
),
// Or if the value changes:
ValueRemovedHandler(
() =>
println("User #1 just removed his/her first name")
)
)(
// Optional cancellation/error handler
Cancelled(
(error: DatabaseError) =>
println(s"Oops, something broke: $e")
)
)
// Make sure to clean up the watcher when you are finished with it.
db.unwatchValue(watcherId)
Listen for changes, additions, or deletions to children at a given path. Children are sub-nodes of an object. Firebase does not use arrays; instead collections are represented as objects, where keys are sequential but not numeric. As such, you can listen in when a new child is added (or changed or deleted).
import com.seancheatham.storage.firebase._
val db: FirebaseDatabase = ???
val watcherId =
db.watchCollection("posts")(
// Attach as many handlers as you want
// For example, when a child is added
ChildAddedHandler {
(post: JsValue) =>
val title =
post.as[Map[String, JsValue]]("title").as[String]
println(s"New post added: $postTitle")
}
)( /* Optional cancellation/error handler */)
// Make sure to clean up the watcher when you are finished with it.
db.unwatchCollection(watcherId)