Farango

A native F# client for ArangoDB

It was developed to fulfill three requirements.

  1. We prefer a bespoke idiomatic F# client over MacGyvering C# libraries.
  2. We want to leverage async to keep our applications non-blocking. That includes using AsyncSeq to return results as they become available.
  3. 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