It was developed to fulfill three requirements.
- We prefer a bespoke idiomatic F# client over MacGyvering C# libraries.
- We want to leverage async to keep our applications non-blocking. That includes using AsyncSeq to return results as they become available.
- As developers, we don't want to be pigeonholed into receiving results in a given construct (Maps or Dictionaries) or with a given libary (Newtonsoft or Chiron.)
We leave it up to client users how they want to parse results.
That being said, Farango is currently a library of convenience.
We implement features as we need them.
Currently, that means that you can CRUD a document as well as query the database.
We are, of course, open to community involvement.
Pro tip Use paket generate-load-scripts
to avoid manually loading all of Farango's dependencies in your .fsx files
Connections
We use dependency injection and include a Connection parameter in every database call.
This makes it easier to test the library as well as any implementation thereof.
It also allows you to create multiple connections (to multiple databases or even Arango instances.)
Connections are made asynchronously and return a Result<Connection, string>
.
1:
2:
3:
4:
|
#load "../Farango/Farango.Connection.fs"
open Farango.Connection
let connection = connect "http[s]://[username]:[password]@[host]:[port]/[database]" |> Async.RunSynchronously
|
Results
Results to all commands and queries are given as JSON strings wrapped in a result.
If the result is a single document it will have the form Result<string, string>
.
If the result is a list of documents it will have the form Result<string list, string>
.
getDocumentCount
returns Result<int, string>
because, you know, that makes sense.
Queries
Queries are given a Connection, query, and optional Map of bindVars, and an optional batchSize.
Queries return all results at once even if the background requests are batched as per batchSize.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
#load "../Farango/Farango.Queries.fs"
open Farango.Queries
async {
match connection with
| Ok connection ->
return! query connection "FOR u IN users RETURN u" None (Some 100)
| Error error -> return Error error
} |> Async.RunSynchronously
|
bindVars allow you to inject variables into queries in a safe manner.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
|
#load "../Farango/Farango.Queries.fs"
open Farango.Queries
async {
match connection with
| Ok connection ->
let bindVars =
Map.empty.
Add("key", (box<string> "12345"))
return! query connection "FOR u IN users FILTER u._key == @key RETURN u" (Some bindVars) (Some 100)
| Error error -> return Error error
} |> Async.RunSynchronously
|
Query Sequences
You can also use query results as a sequence.
They are also given a connection, query, an optional Map of bindVars, and an optional batchSize like a regular query.
You will need to use the AsyncSeq library to manipulate the sequence.
Here, batchSize will determine how many results are returned in each iteration of the sequence.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
|
#r "../packages/FSharp.Control.AsyncSeq/lib/net45/FSharp.Control.AsyncSeq.dll"
open FSharp.Control
async {
match connection with
| Ok connection ->
querySequence connection "FOR u IN users RETURN u" None (Some 100)
|> AsyncSeq.iter (printfn "\n%A\n")
|> Async.Start
| _ -> ()
} |> Async.RunSynchronously
|
Documents
You can CRUD documents by passing a serialized JSON string in as the document.
For example, if we wanted to create, update, replace, and then delete a user from the users collection.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
|
#load "../Farango/Farango.Documents.fs"
open Farango.Documents
async {
match connection with
| Ok connection ->
let! createdDocument = createDocument connection "users" "{\"_key\":\"12345\"}"
let! document = getDocument connection "users" "12345"
let! updatedDocument = updateDocument connection "users" "12345" "{\"username\":\"name\"}"
let! replacedDocument = replaceDocument connection "users" "12345" "{\"username\":\"user\",\"password\":\"pass\"}"
return! deleteDocument connection "users" "12345"
| Error error -> return Error error
} |> Async.RunSynchronously
|
You can create multiple documents using createDocuments
.
Just pass in a serialized JSON string representing an array of documents.
1:
2:
3:
4:
5:
6:
7:
8:
|
async {
match connection with
| Ok connection ->
return! createDocuments connection "users" "[{\"username\":\"user\"},{\"username\":\"name\"}]"
| Error error -> return Error error
} |> Async.RunSynchronously
|
Collections
You can do basic queries on collections.
allDocuments
takes a connection, collection, optional skip, optional limit, and optional batchSize.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
#load "../Farango/Farango.Collections.fs"
open Farango.Collections
async {
match connection with
| Ok connection ->
return! allDocuments connection "users" None None None
| Error error -> return Error error
} |> Async.RunSynchronously
|
Of course, you can also get all documents as a sequence.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
async {
match connection with
| Ok connection ->
allDocumentsSequence connection "users" None None None
|> AsyncSeq.iter (printfn "\n%A\n")
|> Async.Start
| _ -> ()
} |> Async.RunSynchronously
|
Change data capture
You can poll an Arango instance and be notified when documents are inserted/updated or deleted.
A Subscriber is a Change (InsertUpdate | Delete), a Collection (string option), and a function from Message -> unit.
Messages have the same Change and Collection fields as Subscribers.
Messages also have Data which is a JSON string.
This will hold the document in question.
You can parse it however you wish.
You can subscribe to a single collection with Collection = Some "collection" or subscribe to all collections with Collection = None.
You'll see below that we are listening for InsertUpdate events on the users collection and Delete events on every collection in the database.
To start polling just pass a Connection and a list of Subscribers to the start function.
We use mutually recursive async functions to poll the database.
A simple backoff is used and a second is added between polls.
Once new data is found, the backoff resets to zero.
There is a maximum backoff time of 10 seconds.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
#load "../Farango/Farango.Cdc.fs"
open Farango.Cdc
match connection with
| Ok connection ->
let sub1 = { Change = InsertUpdate; Collection = Some "users"; Fn = printfn "\n%A\n" }
let sub2 = { Change = Delete; Collection = None; Fn = printfn "\n%A\n" }
start connection [sub1; sub2]
| _ -> ()
|
namespace Farango
module Connection
from Farango
val connection : obj
Full name: Index.connection
val connect : uri:string -> Async<'a>
Full name: Farango.Connection.connect
Multiple items
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:System.Threading.CancellationToken -> 'T
module Queries
from Farango
val async : AsyncBuilder
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
val query : connection:'a -> query:string -> bindVars:Map<string,obj> option -> batchSize:int option -> Async<'b>
Full name: Farango.Queries.query
union case Option.None: Option<'T>
union case Option.Some: Value: 'T -> Option<'T>
Multiple items
module Map
from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> =
interface IEnumerable
interface IComparable
interface IEnumerable<KeyValuePair<'Key,'Value>>
interface ICollection<KeyValuePair<'Key,'Value>>
interface IDictionary<'Key,'Value>
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
member Add : key:'Key * value:'Value -> Map<'Key,'Value>
member ContainsKey : key:'Key -> bool
override Equals : obj -> bool
member Remove : key:'Key -> Map<'Key,'Value>
...
Full name: Microsoft.FSharp.Collections.Map<_,_>
--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val empty<'Key,'T (requires comparison)> : Map<'Key,'T> (requires comparison)
Full name: Microsoft.FSharp.Collections.Map.empty
val box : value:'T -> obj
Full name: Microsoft.FSharp.Core.Operators.box
Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
Multiple items
namespace FSharp
--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Control
--------------------
namespace Microsoft.FSharp.Control
val querySequence : connection:'a -> query:string -> bindVars:Map<string,obj> option -> batchSize:int option -> AsyncSeq<'b>
Full name: Farango.Queries.querySequence
Multiple items
module AsyncSeq
from FSharp.Control
--------------------
type AsyncSeq<'T> = IAsyncEnumerable<'T>
Full name: FSharp.Control.AsyncSeq<_>
val iter : action:('T -> unit) -> source:AsyncSeq<'T> -> Async<unit>
Full name: FSharp.Control.AsyncSeq.iter
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
static member Async.Start : computation:Async<unit> * ?cancellationToken:System.Threading.CancellationToken -> unit
module Documents
from Farango
val createDocument : connection:'a -> collection:string -> body:string -> Async<obj>
Full name: Farango.Documents.createDocument
val getDocument : connection:'a -> collection:string -> key:string -> Async<obj>
Full name: Farango.Documents.getDocument
val updateDocument : connection:'a -> collection:string -> key:string -> body:string -> Async<obj>
Full name: Farango.Documents.updateDocument
val replaceDocument : connection:'a -> collection:string -> key:string -> body:string -> Async<obj>
Full name: Farango.Documents.replaceDocument
val deleteDocument : connection:'a -> collection:string -> key:string -> Async<obj>
Full name: Farango.Documents.deleteDocument
val createDocuments : ('a -> string -> string -> Async<obj>)
Full name: Farango.Documents.createDocuments
module Collections
from Farango
val allDocuments : connection:'a -> collection:string -> skip:int option -> limit:int option -> batchSize:int option -> Async<'b>
Full name: Farango.Collections.allDocuments
val allDocumentsSequence : connection:'a -> collection:string -> skip:int option -> limit:int option -> batchSize:int option -> AsyncSeq<'b>
Full name: Farango.Collections.allDocumentsSequence
module Cdc
from Farango
type Change =
| InsertUpdate
| Delete
Full name: Farango.Cdc.Change
union case Change.InsertUpdate: Change
union case Change.Delete: Change
val start : connection:'a -> bus:Bus -> unit
Full name: Farango.Cdc.start