cl-mango
2020-09-25
A minimalist CouchDB 2.x database client.
Mango is a client library for CouchDB 2.x (it also happens to be the name of CouchDB's alternate query language)
1Configuration
(eval-when (:compile-toplevel :load-toplevel :execute)
(setf cl-mango:*host* "127.0.0.1")
(setf cl-mango:*port* 5984)
(setf cl-mango:*scheme* :http) ;; or :https
(setf cl-mango:*username* "user")
(setf cl-mango:*password* "password"))
2(defmango ...)
(defmango) macro that generates an orm-ish interface to a class.
(defmango person people
((name :json-type :string
:json-key "name"
:initarg :name
:accessor person-name)
(role :json-type :string
:json-key "role"
:initform "pleb"
:initarg :role
:accessor person-role))
=>
(progn
(defclass person ()
((cl-mango::-id :initarg :-id
:json-type :string
:json-key "_id"
:accessor person--id)
(cl-mango::-rev :initarg :-rev
:json-type :string
:json-key "_rev"
:accessor person-rev)
(type :initarg :type
:json-type :string
:json-key "type"
:initform (string-downcase "person"))
(name :json-type :string
:json-key "name"
:initarg :name
:accessor person-name)
(role :json-type :string
:json-key "role"
:initform "pleb"
:accessor person-role))
(:metaclass json-mop:json-serializable-class))
;;
;; Important notes about this defclass expansion:
;;
;; - type is special as it tells defmodel what class query results
;; are supposed to be. It's filled in automatically. Unless you
;; want to see things explode spectacularly, don't change it.
;;
;; - person--id and person-rev are special to CouchDB. When
;; creating a new record, *don't* fill in "_rev" as CouchDB
;; assigns it a new revision tag on insert and update. The only
;; time I ever find myself reading the rev slot in practice is
;; when I need to delete a record, in which you provide an _id and
;; _rev.
;;
(defun person-get-all ())
;;
;; Doesn't actually get all, only the first 100 as it uses
;; couchdb/database/_all_docs?include_docs=true if you truly want
;; all, I would suggest fetching only the "_id" of all with
;; (person-find) and then iterating over that list. ie.
;;
;; (person-find (list (cons "name" (alist-hash-table ;; this selector can b
;; (list (cons "$exists" 't)))))
;; :limit 1000000 ;; or some similarly large number.
;; :fields (list "_id"))
(defun person-get (id))
;; Fetch a single person record from the database with the given id.
;; CouchDB enforces that all "_id" values are unique, so there will
;; only ever be a single result from this call.
(defun person-put (cl-mango::object))
(defun person-update (cl-mango::object))
;; These two calls are identical in their implementation, however
;; there are two to help with the differences in adding a record and
;; updating a record in CouchDB.
;; - Adding a record without a value for "_rev" adds a new record to
;; the database.
;; - Adding a record with a value in "_rev" means, in CouchDB terms,
;; "This is a new revision of that document." Note that if you
;; update a record (ie. you have a value for "_rev") and it's not
;; the latest and greatest version in the database at that time,
;; you'll get a document update conflict and cl-mango will throw
;; the dreaded "unexpected-http-response" condition. So, yes,
;; it's a bit redundant, but it helps me, so it's staying.
;;
(defmacro person-find (cl-mango::query &rest cl-mango::query-args))
;;
;; This is the main interface for querying objects of your class.
;; It takes the same arguments as (make-selector) above and
;; transparently adds (cons "type" "<class name>") to all queries.
;;
;; Notes:
;; - (defmango) does *not* add an index for "type" in the database.
;; You'll have to go in to the Mango query interface in Fauxton
;; and add one.
;; - If you plan to sort by a given field, you'll need to add a
;; Mango index for that as well or cl-mango will throw an
;; "unexpected-http-response" condition. It's CouchDB, man, not
;; me.
;;
(person-find (list (cons "name" "bob")))
(person-find (list (cons "role" "admin"))
:limit 1
:skip 100
:stable t
:update t
:use-index "index-name"
:r 2
:fields (list "_id" "_rev")
:sort (list (alexandria:alist-hash-table
(list (cons "name" "desc")))))
;;
(defun person-delete (cl-mango::object))
;;
;; Removes the object from the database.
;;
(defmacro person-create (&rest cl-mango::args))
;; Make a new object and add it to the database.
;; There's nothing special about this, and there's nothing preventing you from
;; using (make-instance).
(person-put (make-instance 'person :name "bob"))
3Lower level api
(defmango) is defined in terms of the following functions.
- make-selector selector &key limit fields sort skip
Builds a selector for doc-find.
(make-selector (list (cons "name" "mango"))
:limit 10
:fields (list "_id" "_rev")
:sort '(cons "name" "desc")
:skip 100)
- doc-find database selector
Execute a query against <database>.
(doc-find "test" (make-selector (list (cons "name" "me")))
- doc-get database document-id
Get a single document by the
_id
.
(doc-get "test" "<docid>")
- doc-delete database document-id document-rev
Delete a single document.
- doc-put database json-string
Insert a single document.
;; Assuming you use yason, but as long as the
;; string is well formed JSON, you can use
;; whatever library you want.
(doc-put "test" (with-output-to-string (sink)
(yason:encode
(list (cons "name" "me")
(cons "something" "something else"))
sink)))
- query-view database view index &key parameters
(query-view "test" "reports" "by-person" (list (cons "uid" 12")))