A small Functional Scala 3 wrapper for ArangoDB.
Scaladex | Scaladoc | Maven |
---|---|---|
So, I have been working on a project that utilises ArangoDB and ZIO. I would like to use Arango due to its geographical index support. I'm enjoying coding in functional fashion. Therefore, after some mess and some ideas I have decide to move all codes to an external library and publish it. So, it's the very beginning of this project and I have just done the basic set of features and functionalities to help me in my project. Any help such as ideas, features and fixes are welcome.
Farango(Functional Arango) has built in support for ZIO, Cats Effect, Scala's Future, Try and Either[Throwable, _] types.
Add in your build.sbt
one of the following dependencies:
"one.estrondo.farango" %% "farango" % "0.0.1" // If you want to use just the Scala's types.
"one.estrondo.farango" %% "farango-zio" % "0.0.1" // If you want working with ZIO.
"one.estrondo.farango" %% "farango-cats-effect" % "0.0.1" // If you want Cats Effect.
Farango was written with functional programming style kept in mind. Therefore, it uses the famous Effectfull type F[_]
. In order to use Farango you have to make some imports in your code.
import one.estrondo.farango.*
import one.estrondo.farango.zio.given // If you are using ZIO.
import one.estrondo.farango.cats.effect.given // it you are using Cats Effect.
All examples mentioned above could be found on ZIO example and Cats Effect example.
Farango strives to resemble the ArangoDB's Java Drive design, but to have the ability of create/recreate a database as many times as you want Farango needs to keep all configuration information, because of this you have to create a Farango Config object (one.estrondo.farango.Config
).
val config = Config()
.addHost("localhost", 8529)
.withUser("user")
.withPassword("user password")
.withRootPassword("Arango's root password") // You need this in order to create databases and collections through Farango.
After that it's time to have some fun, or at least attempt to do so. To create a Farango's DB version of Arango's DB you currently need to utilise the object one.estrondo.farango.SyncDB
. Prior to version 7.x ArangoDB Java Driver had async
and sync
clients. However, in the current version of ArangoDB Java Driver only supports a sync
client, in their website they say the support to async
will be re-added in a future version 7.x. Hence, Farango provides a SyncDB
object which acts as a Factory for the one.estrondo.farango.DB
.
Let's remember one small thing, in ArangoDB Java Driver a DB instance represents not a specific database, but it represents the database server.
import one.estrondo.farango.sync.SyncDB
val db = SyncDB(config) // SyncDB will return a Try[DB].
If you are using ZIO you would do like this:
val db = ZIO.fromTry(SyncDB(config))
If you are using Cats Effect you would do like this:
val db = IO.fromTry(SyncDB(config))
Or you are using scala.concurrent.Future
:
val db = Future.fromTry(SyncDB(config))
You are free to choose, or you could use the scala.util.Try
directly or map it to a scala.util.Either
object.
You had made your choice of which framework to use to build your application, let's see some examples.
Let's assume that we want to create a user on the database server. This can be accomplished with createUser(user, password, options)
method. The parameter options
is a UserCreateOptions
and the result type is UserEntity
both from ArangoDB Java Driver. Actually, the result type is a F[UserEntity]
. The parameter options
is optional.
for
entity <- db.createUser("user-login", "user-password")
yield entity.getUser
IMPORTANT: For some operations Farango needs that you define in the Config object the property rootPassword
, because in these situations Farango needs to connect to the database server using the root
account.
For this purpose you have to use the DB.database(String | DBCreateOptions)
method, you can inform a database name for example:
for
database <- db.database("application-database")
...
Or you can use a DBCreateOptions
to create an instance of one.estrondo.farango.Database
.
for
database <- db.database(
DBCreateOptions()
.name("test-database")
.options(DatabaseOptions().sharding("sharding"))
)
...
Creating a database on database server is straightforward with Arango Java Driver. The same applies to the Farango. Simply utilise the Database.create()
method. It is worth noting that Farango needs that Config
object with a rootPassword
defined.
for
database <- db.database("test-database")
.create()
...
Currently Farango only supports document collections.To create a one.estrondo.farango.Collection
you simply need to utilise the database.collection(name, indexes, options)
method. Note that indexes is a Seq[one.estrondo.farango.IndexDescription]
and options is a CollectionCreateOptions
. Both indexes and options are optional.
val indexes = Seq(IndexDescription.Geo(Seq("geom"), GeoIndexOptions().geoJson(true)))
val collection = database
.collection("collection-name", indexes)
As we did with our database, to create a collection on the database server you can utilise the collection.create()
method.
for
collection <- database.collection("collection-name").create()
...
Now we will go through how to create, read, update and delete our documents.
Farango aims to help with the separation between the business layer and the storage layer. To accomplish this Farango employs the mapping process, or transformation. Please, refer the the following section to understand how Farango accomplishes this.
Let's assume we have a document in our business layer which of the type T
, and we want to represent this document in our storage layer as type A
. Furthermore, we want after inserting this document return it as type R
.
To insert a document into the collection, you can use collection.insertDocument
method.
for
entity <- collection.insertDocument[A, R](value) // The type of value is T.
...
The method insertDocument
will receive a value of the type T, and it will convert to A
and store it in the collection. After that it will return a entity that is the type DocumentCreateEntity[R]
, it is worth noting that is a DocumentCreateEntity
of R
.
The method insertDocument
is expecting for two given
objects, one.estrondo.farango.FarangoTransformer[T, A]
and one.estrondo.farango.FarangoTransformer[A, R]
. You can provide your own given objects that implement this Typeclasse, it may feel rather uncomfortable to do so. Imagine an application with a bunch of types and transformers, it is going to be a mess! There is where our friend Ducktape comes to rescue, thanks Ducktape!
Ducktape as the creators say is "ducktape is a library for boilerplate-less and configurable transformations between case classes and enums/sealed traits for Scala 3. Directly inspired by chimney."
First add Farango's ducktape extension in your build.sbt
.
libraryDependencies += "one.estrondo" %% "farango-ducktape" % "0.0.1"
Once you have added Farango's ducktape extension to your build you can use it in two basic ways. First, you can import one.estrondo.farango.ducktape.given
and where Farango needs a FarangoTransformer
one given
object will be provided automatically, but it is worth attention. Farango utilises ducktape to generate a Transformer
, so please refer to the ducktape documention.
Let's have a look at an example:
import one.estrondo.farango.ducktape.given
val postIt = PostIt("My Post-it")
for
createEntity <- collection
.insertDocument[StoredPostIt, CreatedPostIt](postIt)
...
In the example above we are inserting a document of type PostIt
, it will be transformed into a StoredPostIt
automatically and after that the StoredPostIt
value from the collection will be transformed into a CreatedPostIt
as well.
So, if you require more control or it is impossible to create an automatic transformer, you can configure a new one using the object one.estrondo.farango.ducktape.DucktapeTransformer
. Please, read ducktape's documentation to be introduced.
An example configuring a new Transformer.
import one.estrondo.farango.ducktape.DucktapeTransformer
val applePostIt = ApplePostIt("My Post-it too.")
given FarangoTransformer[ApplePostIt, StoredPostIt] = DucktapeTransformer[ApplePostIt, StoredPostIt](
Field.renamed(_.id, _.ID)
)
for
createEntity <- collection
.insertDocument[StoredPostIt, CreatedPostIt](applePostIt)
...
In the example above we are receiving an ApplePostIt
document which has an attribute 'ID' instead 'id' as in StoredPostIt
, because of this ducktape can't create an automatic transformer. Hence, we have to provide one semi-automatic transformer, in the example we simply inform ducktape that the attribute 'ID' in ApplePostIt
was renamed as 'id' in StoredPostIt
.
Example.
for
getPostIt <- collection.getDocument[StoredPostIt, PostIt](key) // It returns a F[Option[PostIt]]
...
Farango returns all queries as Streams.
Farango returns a zio.stream.ZStream[Any, Throwable, R]
.
import one.estrondo.farango.zio.given
for
result <- database
.query[StoredPostIt, PostIt](
"FOR postIt IN @@collection FILTER postIt.id == @id RETURN postIt",
Map(
"@collection" -> "collection",
"id" -> postIt.id
)
)
.runCollect
...
In the example above Farango returns a ZStream[Any, Throwable, PostIt]
, note we collecting all objects for the example's sake.
Farango returns a fs2.Stream[IO, R]
.
import one.estrondo.farango.cats.effect.given
for
result <- database
.query[StoredPostIt, PostIt](
"FOR postIt IN @@collection FILTER postIt.id == @id RETURN postIt",
Map(
"@collection" -> "collection",
"id" -> postIt.id
)
)
.compile
.toList
...
In the example above Farango returns a fs2.Stream[IO, PostIt]
, note we are converting the stream to a list for the example's sake.
Let's have a look at an example:
val postIt = PostIt("My Post-it")
val newLastUpdate = LocalDateTime.now()
for
updateEntity <- collection.updateDocument[StoredPostIt, UpdateContent, UpdatedPostIt](
documentKey,
postIt.copy(content = "New Content", lastUpdate = newLastUpdate),
DocumentUpdateOptions()
.returnOld(true)
.returnNew(true)
)
_ = assert(updateEntity.getOld == UpdatedPostIt(postIt.id, "My Post-it"))
_ = assert(updateEntity.getNew == UpdatedPostIt(postIt.id, "New Content"))
...
In the example above Farango transforms a PostIt
into UpdateContent
and partially updating the document in the collection, see ArangoDB Java Driver documentation.
Example:
val postIt = PostIt("My Post-it")
val newLastUpdate = LocalDateTime.now()
// document was updated.
for
deleteEntity <- collection.deleteDocument[StoredPostIt, DeletedPostIt](
documentEntity,
DocumentDeleteOptions().returnOld(true)
)
_ = assert(deleteEntity.getOld == DeletedPostIt(postIt.id, "New Content", newLastUpdate))
...
Farango is in its early stages, for example, it currently supports only document collections, there is no support for edge collections and there are many functionalities that Arango's Java Driver provides which are not covered yet by Farango. If you believe this project could be helpful and you would like to contribute, your help is more than welcome.