A Clojure library designed to consume a RavenDB HTTP api.
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
doc
scripts
src/clj_ravendb
test/clj_ravendb
.gitignore
.travis.yml
LICENSE
README.md
project.clj

README.md

clj-ravendb

A Clojure library designed to consume a RavenDB HTTP api.

API Docs

Status

Build Status Dependency Status Dependency Status

This is currently a work in progress and under active development, it is liable to substantial change and currently has the following features:

  • Load documents
  • Create documents
  • Update documents
  • Delete documents
  • Query indexes
    • Range queries
  • Create indexes
  • Delete indexes
  • OAuth Support
  • Replication aware
  • Watch document(s) for changes
  • Watch index queries for changes

Installation

clj-ravendb is available from Clojars

Add the following to project.clj :dependencies:

Clojars Project

Usage

(:require [clj-ravendb.client :refer :all]))

Getting a RavenDB client:

(def northwind (client "http://localhost:8080" "northwind"))

Loading some documents:

(load-documents northwind ["employees/1" "employees/2"])

Returns a map with a sequence of results like:

{:status 200,
 :results
 ({:id "employees/1",
   :last-modified-date "2013-11-12T14:48:11.2943076Z",
   :etag "01000000-0000-0001-0000-00000000006A",
   :document
   {:Territories ["06897" "19713"],
    :HomePhone "(206) 555-9857",
    :ReportsTo "employees/1",
    :LastName "Davolio",
    :Address
    {:Line1 "507 - 20th Ave. E.\r\nApt. 2A",
     :Line2 nil,
     :City "Seattle",
     :Region "WA",
     :PostalCode "98122",
     :Country "USA"},
    :FirstName "Nancy",
    :Birthday "1948-12-08T00:00:00.0000000",
    :Title "Sales Representative",
    :Extension "5467",
    :Notes nil,
    :HiredAt "1992-05-01T00:00:00.0000000"}}
  {:id "employees/2",
   :last-modified-date "2013-11-12T14:48:11.2943076Z",
   :etag "01000000-0000-0001-0000-000000000069",
   :document
   {:Territories
    ["01581" "01730" "01833" "02116" "02139" "02184" "40222"],
    :HomePhone "(206) 555-9482",
    :ReportsTo nil,
    :LastName "Fuller",
    :Address
    {:Line1 "908 W. Capital Way",
     :Line2 nil,
     :City "Tacoma",
     :Region "WA",
     :PostalCode "98401",
     :Country "USA"},
    :FirstName "Andrew",
    :Birthday "1952-02-19T00:00:00.0000000",
    :Title "Vice President, Sales",
    :Extension "3457",
    :Notes nil,
    :HiredAt "1992-08-14T00:00:00.0000000"}})}

Putting a document:

(put-document! northwind "Employees/10" { :FirstName "David" :LastName "Smith" :age 50 })

Returns a map with a key to indicate the HTTP status:

{:status 200}

Deleting a document:

(bulk-operations! northwind [{:method "DELETE"
                              :id "Key1"}])

Returns a map with a key to indicate the HTTP status:

{:status 200}

Querying an index:

(query-index northwind {:index "ByCompany" :query {:Count 10}})

;; Sort
(query-index northwind {:index "ByCompany" :query {:Count 10} :sort-by :Total })

;; Sort Descending
(query-index northwind {:index "ByCompany" :query {:Count 10} :sort-by :-Total })

;; Paging
(query-index northwind {:index "ByCompany" :query {:Count 10} :sort-by :Total :page-size 10 :start 1})

;; Range
(query-index northwind {:index "ByCompany" :query {:Count [:range 10 90}})

;; Raw lucene
(query-index northwind {:index "ByCompany" :query "Count:10 AND Value:5"})

;; By default if the index is stale (query-index) will retry 5 times, waiting
;; 100 milliseconds between each try.

;; If the index is stale retry a maximum of 10 times.
(query-index northwind { :index "ByCompany" :query {:Count 10}} { :max-attempts 10 })

;; If the index is stale retry every 500 milliseconds.
(query-index northwind { :index "ByCompany" :query {:Count 10}} { :wait 500 })

Returns a map with a sequence of results like:

{:status 200,
 :stale? false,
 :total-results 10,
 :results
 ({:id "companies/38", :etag "01000000-0000-0001-0000-000000000069" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/38", :Count 10.0, :Total 6146.3}}
  {:id "companies/49", :etag "01000000-0000-0001-0000-000000000060" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/49", :Count 10.0, :Total 7176.215}}
  {:id "companies/11", :etag "01000000-0000-0001-0000-000000000068" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/11", :Count 10.0, :Total 6089.9}}
  {:id "companies/30", :etag "01000000-0000-0001-0000-000000000067" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/30", :Count 10.0, :Total 11446.36}}
  {:id "companies/84", :etag "01000000-0000-0001-0000-000000000066" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/84", :Count 10.0, :Total 9182.43}}
  {:id "companies/56", :etag "01000000-0000-0001-0000-000000000065" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/56", :Count 10.0, :Total 12496.2}}
  {:id "companies/55", :etag "01000000-0000-0001-0000-000000000046" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/55", :Count 10.0, :Total 15177.4625}}
  {:id "companies/86", :etag "01000000-0000-0001-0000-000000000064" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/86", :Count 10.0, :Total 9588.425}}
  {:id "companies/59", :etag "01000000-0000-0001-0000-000000000063" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/59", :Count 10.0, :Total 23128.86}}
  {:id "companies/68", :etag "01000000-0000-0001-0000-000000000062" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/68", :Count 10.0, :Total 19343.779}}
  {:id "companies/80", :etag "01000000-0000-0001-0000-000000000061" last-modified-date "2013-11-12T14:48:11.2943076Z" :document {:Company "companies/80", :Count 10.0, :Total 10812.15}})}

Creating an index:

(put-index! northwind {:index "ExpensiveSweetAndSavouryProductsWithLowStockAndRunningOut" ;; the index name
                       :from :Products  ;; if :from is not specified then all doc collections will be covered.
                       :where [[:> :PricePerUnit 20] [:< :UnitsInStock 10] [:== :UnitsOnOrder 0] [:== :Category "categories/2"]] ;; the where clauses
                       :select [:Name]} ;; the fields to select

;; Specify an analyzed field
(put-index! northwind {:index "ExpensiveSweetAndSavouryProductsWithLowStockAndRunningOut" ;; the index name
                       :from :Products  ;; if :from is not specified then all doc collections will be covered.
                       :where [[:> :PricePerUnit 20] [:< :UnitsInStock 10] [:== :UnitsOnOrder 0] [:== :Category "categories/2"]] ;; the where clauses
                       :select [:Name :PricePerUnit] ;; the fields to select
                       :fields [:PricePerUnit {:Indexing :Analyzed :Analyzer :StandardAnalyzer :Storage :Yes}]} ;; PricePerUnit will now be analyzed and usable in for example a range query

Returns a map with a key to indicate the HTTP status:

{:status 200}

Deleting an index:

(delet-index! northwind "ExpensiveSweetAndSavouryProductsWithLowStockAndRunningOut")

Returns a map with a key to indicate the HTTP status:

{:status 204}

Watching for document changes:

;; Watching documents makes use of core.async channels.
;; You can either pass in a channel or have watch-documents create one.
;; It creates a future that continuously calls load-document and monitors
;; the results, each time they are different they are put on the channel.
(def watcher (watch-document northwind ["Companies/80", "Companies/79"]))
(let [ch (chan)]
 (def watcher (watch-documents northwind ["Companies/80","Companies/79"] ch)))

;; You can optionally tell the watcher to wait x milliseconds between 'checks'.
(def watcher (watch-documents northwind ["Companies/80","Companies/79"] ch {:wait 1000}))

;; If you dont pass in a channel, you can access the newly created one
(let [ch (:channel watcher)])

;; You can take from the channel like so
(def change (<!! ch))

;; Or non-blocking in a go block
(go (<! ch))

;; You can stop watching using :stop, this will close the channel and stop
;; the future
((:stop watcher))

Watching for index changes:

;; Watching documents makes use of core.async channels.
;; You can either pass in a channel or have watch-documents create one.
;; It creates a future that continuosly calls load-document and monitors
;; the results, each time they are different they are put on the channel.
(def watcher (watch-index northwind {:index "SomeIndexToWatch"}))

(let [ch (chan)]
 (def watcher (watch-index northwind {:index "SomeIndexToWatch"} ch)))

;; You can optionally tell the watcher to wait x milliseconds between 'checks'.
(def watcher (watch-index northwind {: index "SomeIndexToWatch"} ch {:wait 1000}))

;; If you dont pass in a channel, you can access the newly created one
(let [ch (:channel watcher)])

;; You can take from the channel like so
(def change (<!! ch))

;; Or non-blocking in a go block
(go (<! ch))

;; You can stop watching using :stop, this will close the channel and stop
;; the future
((:stop watcher))

Options

There are a number of "configuration" options that can be used when creating a client:

OAuth

To create a client that supports OAuth:

(def northwind (client "http://localhost:8080" "northwind" {:enable-oauth? true :oauth-url "http://localhost:8081/oauth" :api-key "API-KEY"}

Depending on the usage pattern for the client it is possible that the oauth token will expire. By default clj-ravendb will request a new token every 10 minutes. This can be configured using the :oauth-expiry-seconds option. For example:

(def northwind (client "http://localhost:8080" "northwind" {:enable-oauth? true :oauth-url "http://localhost:8081/oauth" :oauth-expiry-seconds 1200 :api-key "API-KEY"}

Caching

To create a client that supports caching:

(def northwind (client "http://localhost:8080" "northwind" {:caching :aggressive}))

When this option is used documents loaded using load-documents will be cached. put-document! and bulk-operations! will update this local cache as well as the server.

Replication

To create a client that supports replication:

(def northwind (client "http://localhost:8080" "northwind" {:replicated? true}))

When this option is used creating the client will also query the master url for replication destinations. The client will be represented by a map that looks like:

{:replicated? true
 :master-only-write? true
 :address "http://localhost:8080"
 :replications ("http://localhost:8081" "http://localhost:8082")}

When this client is used to load-documents or query-index if the master is down then one of the replications will be used.

Master Only Write

If you've created a client that supports replication by default write operations will only go to the master, you can change this behaviour using the following:

(def northwind (client "http://localhost:8080" "northwind" {:replicated? true :master-only-write? false}))

The client will be represented by a map that looks like:

{:replicated? true
 :master-only-write? false
 :address "http://localhost:8080"
 :replications ("http://localhost:8081" "http://localhost:8082")}

When this client is used to put-document!, put-index! or for bulk-operations! if the master is down then one of the replications will be used for write operations.

Build & Test

lein test

The tests for this project run agaist a cloud hosted RavenDB 3.0 instance at RavenHQ and use an OAuth version of the client.

All functionality has been tested against RavenDB 2.5 as well.

License

Copyright © 2017 Mark Woodhall

Released under the MIT License: http://www.opensource.org/licenses/mit-license.php