/src/core/document.lisp

http://github.com/sykopomp/chillax · Lisp · 219 lines · 185 code · 22 blank · 12 comment · 9 complexity · 2c7be85bf087981c9f3148bdec78967a MD5 · raw file

  1. (in-package :chillax.core)
  2. ;;;
  3. ;;; Document errors
  4. ;;;
  5. (define-condition document-error (couchdb-error) ())
  6. (define-condition document-not-found (document-error)
  7. ((id :initarg :id :reader document-404-id)
  8. (db :initarg :db :reader document-404-db))
  9. (:report (lambda (e s)
  10. (format s "No document with id ~S was found in ~A"
  11. (document-404-id e)
  12. (document-404-db e)))))
  13. (define-condition document-conflict (document-error)
  14. ((conflicting-doc :initarg :doc :reader conflicting-document)
  15. (conflicting-doc-id :initarg :id :reader conflicting-document-id))
  16. (:report (lambda (e s)
  17. (format s "Revision for ~A conflicts with latest revision for~@
  18. document with ID ~S"
  19. (conflicting-document e)
  20. (conflicting-document-id e)))))
  21. ;;;
  22. ;;; Direct Document API
  23. ;;;
  24. (defun get-document-revision (db doc-id &key (errorp t))
  25. "Quickly fetches the latest revision for DOC-ID. If ERRORP is NIL, this can be used to quickly
  26. test the existence of a document."
  27. (multiple-value-bind (x status-code headers)
  28. (http-request (strcat (db-uri db) "/" (url-encode (princ-to-string doc-id)))
  29. :method :head)
  30. (declare (ignore x))
  31. (case (or (cdr (assoc status-code +status-codes+ :test #'=))
  32. (error "Unknown status code: ~A." status-code))
  33. (:ok (dequote (cdr (assoc :etag headers))))
  34. (:not-found (when errorp (error 'document-not-found :db db :id doc-id))))))
  35. (defun get-document (db id &key attachmentsp (errorp t) params)
  36. "Finds a CouchDB document in DB, named by ID. PARAMS should be an alist containing the parameters
  37. for the HTTP GET request. If ATTACHMENTSP is TRUE, the document's attachments will be included in
  38. their entirety in their base64-encoded version. It is not recommended you use this unless you really
  39. know what you're doing. If ERRORP is NIL, GET-DOCUMENT will simply return NIL on 404."
  40. (handle-request (response db (url-encode (princ-to-string id))
  41. :parameters (if attachmentsp
  42. (cons (cons "attachments" "true") params)
  43. params))
  44. (:ok response)
  45. (:not-found (when errorp (error 'document-not-found :db db :id id)))))
  46. (defun put-document (db id doc &key batch-ok-p)
  47. "Puts a document into DB, using ID."
  48. (handle-request (response db (url-encode (princ-to-string id)) :method :put :content doc
  49. :parameters (when batch-ok-p '(("batch" . "ok"))))
  50. ((:created :accepted) response)
  51. (:conflict (error 'document-conflict :id id :doc doc))))
  52. (defun post-document (db doc)
  53. "POSTs a document into DB. CouchDB will automatically assign a UUID if the document does not
  54. already exist. Note that using this function is discouraged in the CouchDB documentation, since it
  55. may result in duplicate documents because of proxies and other network intermediaries. If what you
  56. need is to create a new document with a generated id, consider using GET-UUIDS with PUT-DOCUMENT."
  57. (handle-request (response db "" :method :post :content doc)
  58. ((:created :accepted) response)
  59. (:conflict (error 'document-conflict :doc doc))))
  60. (defun delete-document (db id revision)
  61. "Deletes an existing document."
  62. (handle-request (response db
  63. (strcat (url-encode (princ-to-string id))
  64. "?rev="
  65. (url-encode (princ-to-string revision)))
  66. :method :delete)
  67. (:ok response)
  68. (:not-found (error 'document-not-found :db db :id id))))
  69. (defun copy-document (db from-id to-id &key revision)
  70. "Copies a document's content in-database."
  71. (handle-request (response db (url-encode (princ-to-string from-id)) :method :copy
  72. :additional-headers `(("Destination" . ,(princ-to-string to-id)))
  73. :parameters `(,(when revision `("rev" . ,revision))))
  74. (:created response)
  75. (:not-found (error 'document-not-found :db db :id from-id))))
  76. ;;;
  77. ;;; Bulk Document API
  78. ;;;
  79. (defun all-documents (db &rest all-keys)
  80. "Requests the _all_docs document. ALL-KEYS correspond to GET-DOCUMENT's keyword arguments."
  81. (apply #'get-document db "_all_docs" all-keys))
  82. (defun batch-get-documents (db doc-ids)
  83. "Uses _all_docs to quickly fetch the given DOC-IDs in a single request. Note that this function
  84. will NOT signal a DOCUMENT-NOT-FOUND error when one or more DOC-IDs are not found. Instead, the
  85. results will be returned, and it's the user's responsibility to deal with any missing docs."
  86. (handle-request (response db "_all_docs" :method :post
  87. :parameters '(("include_docs" . "true"))
  88. :content (format nil "{\"keys\":~S}"
  89. (data->json (database-server db) doc-ids))
  90. :convert-data-p nil)
  91. (:ok response)))
  92. (defun bulk-post-documents (db documents &key all-or-nothing-p)
  93. "Allows you to update or submit multiple documents at the same time, using CouchDB's _bulk_docs
  94. API. In order to delete a document through this API, the document must have a _document attribute
  95. with JSON 'true' as its value (note that what gets translated into 'true' depends on the server).
  96. DOCUMENTS must be a sequence or sequence-like (depending on what DATA->JSON will do to it).
  97. If ALL-OR-NOTHING-P is true, the entire submission will fail if a single one fails."
  98. (let ((as-json (data->json (database-server db) documents)))
  99. (handle-request (response db "_bulk_docs" :method :post
  100. :content (with-output-to-string (s)
  101. (princ "{\"docs\":" s)
  102. (princ as-json s)
  103. (when all-or-nothing-p
  104. (princ ",\"all_or_nothing\":true" s))
  105. (princ "}" s))
  106. :convert-data-p nil)
  107. ((:ok :accepted :created) response))))
  108. ;;;
  109. ;;; Standalone Attachments
  110. ;;;
  111. (defun put-attachment (db doc-id attachment-name data &key rev (content-type "application/octet-stream"))
  112. "Adds DATA as an attachment. DATA can be a number of things:
  113. * String or sequence of octets - DATA will be sent as-is directly to the server (using
  114. EXTERNAL-FORMAT-OUT for strings).
  115. * Stream - The stream will be read until EOF is reached.
  116. * Pathname - The file the pathname denotes will be opened and its data uploaded.
  117. * Function designator - The corresponding function will be called with one argument, the
  118. stream to the server, to which it should send data.
  119. If the document already exists, REV is required. This function can be used on non-existent
  120. documents. If so, REV is not needed, and a document will be created automatically, and the
  121. attachment associated with it.
  122. The CONTENT-TYPE should be a string specifying the content type for DATA."
  123. (multiple-value-bind (response status-code)
  124. (http-request (strcat (server-uri (database-server db))
  125. "/"
  126. (database-name db)
  127. "/"
  128. (url-encode (princ-to-string doc-id))
  129. "/"
  130. attachment-name)
  131. :method :put
  132. :parameters (when rev
  133. `(("rev" . ,rev)))
  134. :content data
  135. :content-type content-type)
  136. (case (or (cdr (assoc status-code +status-codes+ :test #'=))
  137. (error "Unknown status code: ~A. HTTP Response: ~A"
  138. status-code response))
  139. (:ok (json->data (database-server db) response))
  140. (:created (json->data (database-server db) response))
  141. (:not-found (error 'document-not-found :db db :id doc-id))
  142. (otherwise (error 'unexpected-response :status-code status-code :response response)))))
  143. (defun get-attachment (db doc-id attachment-name)
  144. "Returns 3 values:
  145. 1. STREAM - An open flexi-stream that can be READ. In order to read straight binary data, you must
  146. first fetch the underlying stream with FLEXI-STREAMS:FLEXI-STREAM-STREAM.
  147. 2. MUST-CLOSE-P - A boolean. If TRUE, the user must CLOSE this stream themselves
  148. once reading is done.
  149. 3. CONTENT-LENGTH - Declared content length for the incoming data."
  150. (multiple-value-bind (response status-code headers fourth fifth must-close-p)
  151. (http-request (strcat (server-uri (database-server db))
  152. "/"
  153. (database-name db)
  154. "/"
  155. (url-encode (princ-to-string doc-id))
  156. "/"
  157. attachment-name)
  158. :want-stream t)
  159. (declare (ignore fourth fifth))
  160. (case (or (cdr (assoc status-code +status-codes+ :test #'=))
  161. (error "Unknown status code: ~A. HTTP Response: ~A"
  162. status-code response))
  163. (:ok (values response must-close-p
  164. (let ((content-length (cdr (assoc :content-length headers))))
  165. (when content-length (parse-integer content-length)))))
  166. (:not-found (error 'document-not-found :db db :id doc-id))
  167. (otherwise (when (streamp response) (close response))
  168. (error 'unexpected-response :status-code status-code :response response)))))
  169. (defun delete-attachment (db doc-id attachment-name doc-revision)
  170. "Deletes an attachment from a document. DOC-REVISION must be the latest revision for the document."
  171. (multiple-value-bind (response status-code)
  172. (http-request (strcat (server-uri (database-server db))
  173. "/"
  174. (database-name db)
  175. "/"
  176. (url-encode (princ-to-string doc-id))
  177. "/"
  178. attachment-name)
  179. :method :delete
  180. :parameters `(("rev" . ,doc-revision)))
  181. (case (or (cdr (assoc status-code +status-codes+ :test #'=))
  182. (error "Unknown status code: ~A. HTTP Response: ~A"
  183. status-code response))
  184. (:ok (json->data (database-server db) response))
  185. (:not-found (error 'document-not-found :db db :id doc-id))
  186. (otherwise (error 'unexpected-response :status-code status-code :response response)))))
  187. (defun copy-attachment (db doc-id attachment-name output-stream &key (max-buffer-size 4096))
  188. "Copies data from the named attachment to OUTPUT-STREAM. Returns the number of bytes copied."
  189. (multiple-value-bind (attachment-stream must-close-p content-length)
  190. (get-attachment db doc-id attachment-name)
  191. (unwind-protect
  192. (if (plusp content-length)
  193. (copy-stream (flex:flexi-stream-stream attachment-stream) output-stream
  194. :buffer-size (mod content-length max-buffer-size))
  195. 0)
  196. (when must-close-p
  197. (close attachment-stream)))))