module Database.CouchDB.Unsafe
(
createDB
, dropDB
, getAllDBs
, newNamedDoc
, newDoc
, updateDoc
, bulkUpdateDocs
, deleteDoc
, forceDeleteDoc
, getDocPrim
, getDocRaw
, getDoc
, getAndUpdateDoc
, getAllDocIds
, getAllDocs
, CouchView (..)
, newView
, queryView
, queryViewKeys
) where
import Database.CouchDB.HTTP
import Control.Monad
import Control.Monad.Trans (liftIO)
import Data.Maybe (fromJust, mapMaybe, isNothing)
import Text.JSON
import qualified Data.List as L
assertJSObject :: JSValue -> CouchMonad JSValue
assertJSObject v@(JSObject _) = return v
assertJSObject o = fail $ "expected a JSON object; received: " ++ encode o
couchResponse :: String -> [(String,JSValue)]
couchResponse respBody = case decode respBody of
Error s -> error $ "couchResponse: s"
Ok r -> fromJSObject r
request' :: String -> RequestMethod -> CouchMonad (Response String)
request' path method = request path [] method [] ""
createDB :: String -> CouchMonad ()
createDB name = do
resp <- request' name PUT
unless (rspCode resp == (2,0,1)) $
error (rspReason resp)
dropDB :: String -> CouchMonad Bool
dropDB name = do
resp <- request' name DELETE
case rspCode resp of
(2,0,0) -> return True
(4,0,4) -> return False
otherwise -> error (rspReason resp)
getAllDBs :: CouchMonad [JSString]
getAllDBs = do
response <- request' "_all_dbs" GET
case rspCode response of
(2,0,0) ->
case decode (rspBody response) of
Ok (JSArray dbs) -> return [db | JSString db <- dbs]
otherwise -> error "Unexpected couch response"
otherwise -> error (show response)
newNamedDoc :: (JSON a)
=> String
-> String
-> a
-> CouchMonad (Either String JSString)
newNamedDoc dbName docName body = do
obj <- assertJSObject (showJSON body)
r <- request (dbName ++ "/" ++ docName) [] PUT [] (encode obj)
case rspCode r of
(2,0,1) -> do
let result = couchResponse (rspBody r)
let (JSString rev) = fromJust $ lookup "rev" result
return (Right rev)
(4,0,9) -> do
let result = couchResponse (rspBody r)
let errorObj (JSObject x) = fromJust . lookup "reason"$ fromJSObject x
errorObj x = x
let (JSString reason) = errorObj . fromJust $ lookup "error" result
return $ Left (fromJSString reason)
otherwise -> error (show r)
updateDoc :: (JSON a)
=> String
-> (JSString,JSString)
-> a
-> CouchMonad (Maybe (JSString,JSString))
updateDoc db (doc,rev) val = do
let (JSObject obj) = showJSON val
let doc' = fromJSString doc
let obj' = ("_id",JSString doc):("_rev",JSString rev):(fromJSObject obj)
r <- request (db ++ "/" ++ doc') [] PUT [] (encode $ toJSObject obj')
case rspCode r of
(2,0,1) -> do
let result = couchResponse (rspBody r)
let (JSString rev) = fromJust $ lookup "rev" result
return $ Just (doc,rev)
(4,0,9) -> return Nothing
otherwise ->
error $ "updateDoc error.\n" ++ (show r) ++ rspBody r
bulkUpdateDocs :: (JSON a)
=> String
-> [a]
-> CouchMonad (Maybe [Either JSString (JSString, JSString)])
bulkUpdateDocs db docs = do
let obj = [("docs", docs)]
r <- request (db ++ "/_bulk_docs") [] POST [] (encode $ toJSObject obj)
case rspCode r of
(2,0,1) -> do
let Ok results = decode (rspBody r)
return $ Just $
map (\result ->
case (lookup "id" result,
lookup "rev" result) of
(Just id, Just rev) -> Right (id, rev)
_ -> Left $ fromJust $ lookup "error" result
) results
(4,0,9) -> return Nothing
otherwise ->
error $ "updateDoc error.\n" ++ (show r) ++ rspBody r
forceDeleteDoc :: String
-> String
-> CouchMonad Bool
forceDeleteDoc db doc = do
r <- getDocPrim db doc
case r of
Just (id,rev,_) -> deleteDoc db (id,rev)
Nothing -> return False
deleteDoc :: String
-> (JSString,JSString)
-> CouchMonad Bool
deleteDoc db (doc,rev) = do
r <- request (db ++ "/" ++ (fromJSString doc)) [("rev",fromJSString rev)]
DELETE [] ""
case rspCode r of
(2,0,0) -> return True
otherwise -> fail $ "deleteDoc failed: " ++ (show r)
newDoc :: (JSON a)
=> String
-> a
-> CouchMonad (JSString,JSString)
newDoc db doc = do
obj <- assertJSObject (showJSON doc)
r <- request db [] POST [] (encode obj)
case rspCode r of
(2,0,1) -> do
let result = couchResponse (rspBody r)
let (JSString rev) = fromJust $ lookup "rev" result
let (JSString id) = fromJust $ lookup "id" result
return (id,rev)
otherwise -> error (show r)
getDoc :: (JSON a)
=> String
-> String
-> CouchMonad (Maybe (JSString,JSString,a))
getDoc dbName docName = do
r <- request' (dbName ++ "/" ++ docName) GET
case rspCode r of
(2,0,0) -> do
let result = couchResponse (rspBody r)
let (JSString rev) = fromJust $ lookup "_rev" result
let (JSString id) = fromJust $ lookup "_id" result
case readJSON (JSObject $ toJSObject result) of
Ok val -> return $ Just (id, rev, val)
val -> fail $ "error parsing: " ++ encode (toJSObject result)
(4,0,4) -> return Nothing
otherwise -> error (show r)
getDocPrim :: String
-> String
-> CouchMonad (Maybe (JSString,JSString,[(String,JSValue)]))
getDocPrim db doc = do
r <- request' (db ++ "/" ++ doc) GET
case rspCode r of
(2,0,0) -> do
let obj = couchResponse (rspBody r)
let ~(JSString rev) = fromJust $ lookup "_rev" obj
let ~(JSString id) = fromJust $ lookup "_id" obj
return $ Just (id,rev,obj)
(4,0,4) -> return Nothing
code -> fail $ "getDocPrim: " ++ show code ++ " error"
getDocRaw :: String -> String -> CouchMonad (Maybe String)
getDocRaw db doc = do
r <- request' (db ++ "/" ++ doc) GET
case rspCode r of
(2,0,0) -> do
return $ Just (rspBody r)
(4,0,4) -> return Nothing
code -> fail $ "getDocRaw: " ++ show code ++ " error"
getAndUpdateDoc :: (JSON a)
=> String
-> String
-> (a -> IO a)
-> CouchMonad (Maybe String)
getAndUpdateDoc db docId fn = do
r <- getDoc db docId
case r of
Just (id,rev,val) -> do
val' <- liftIO (fn val)
r <- updateDoc db (id,rev) val'
case r of
Just (id,rev) -> return (Just $ fromJSString rev)
Nothing -> return Nothing
Nothing -> return Nothing
allDocRow :: JSValue -> Maybe JSString
allDocRow (JSObject row) = case lookup "key" (fromJSObject row) of
Just (JSString s) -> let key = fromJSString s
in case key of
'_':_ -> Nothing
otherwise -> Just s
Just _ -> error $ "key not a string in row " ++ show row
Nothing -> error $ "no key in a row " ++ show row
allDocRow v = error $ "expected row to be an object, received " ++ show v
getAllDocIds ::String
-> CouchMonad [JSString]
getAllDocIds db = do
response <- request' (db ++ "/_all_docs") GET
case rspCode response of
(2,0,0) -> do
let result = couchResponse (rspBody response)
let (JSArray rows) = fromJust $ lookup "rows" result
return $ mapMaybe allDocRow rows
otherwise -> error (show response)
data CouchView = ViewMap String String
| ViewMapReduce String String String
couchViewToJSON :: CouchView -> (String,JSValue)
couchViewToJSON (ViewMap name fn) = (name,JSObject $ toJSObject fn') where
fn' = [("map", JSString $ toJSString fn)]
couchViewToJSON (ViewMapReduce name m r) =
(name, JSObject $ toJSObject obj) where
obj = [("map", JSString $ toJSString m),
("reduce", JSString $ toJSString r)]
newView :: String
-> String
-> [CouchView]
-> CouchMonad ()
newView dbName viewName views = do
let content = map couchViewToJSON views
body = toJSObject
[("language", JSString $ toJSString "javascript"),
("views", JSObject $ toJSObject content)]
path = "_design/" ++ viewName
result <- newNamedDoc dbName path
(JSObject body)
case result of
Right _ -> return ()
Left err -> do
let update x = return . toJSObject . map replace $ fromJSObject x
replace ("views", JSObject v) =
("views", JSObject . toJSObject . unite $ fromJSObject v)
replace x = x
unite x = L.nubBy (\(k1, _) (k2, _) -> k1 == k2) $ content ++ x
res <- getAndUpdateDoc dbName path update
when (isNothing res) (error "newView: creation of the view failed")
toRow :: JSON a => JSValue -> (JSString,a)
toRow (JSObject objVal) = (key,value) where
obj = fromJSObject objVal
key = case lookup "id" obj of
Just (JSString s) -> s
Just v -> error $ "toRow: expected id to be a string, got " ++ show v
Nothing -> error $ "toRow: row does not have an id field in "
++ show obj
value = case lookup "value" obj of
Just v -> case readJSON v of
Ok v' -> v'
Error s -> error s
Nothing -> error $ "toRow: row does not have a value in " ++ show obj
toRow val =
error $ "toRow: expected row to be an object, received " ++ show val
getAllDocs :: JSON a
=> String
-> [(String, JSValue)]
-> CouchMonad [(JSString, a)]
getAllDocs db args = do
let args' = map (\(k,v) -> (k,encode v)) args
let url' = concat [db, "/_all_docs"]
r <- request url' args' GET [] ""
case rspCode r of
(2,0,0) -> do
let result = couchResponse (rspBody r)
let (JSArray rows) = fromJust $ lookup "rows" result
return $ map toRowDoc rows
otherwise -> error $ "getAllDocs: " ++ show r
toRowDoc :: JSON a => JSValue -> (JSString,a)
toRowDoc (JSObject objVal) = (key,value) where
obj = fromJSObject objVal
key = case lookup "id" obj of
Just (JSString s) -> s
Just v -> error $ "toRowDoc: expected id to be a string, got " ++ show v
Nothing -> error $ "toRowDoc: row does not have an id field in "
++ show obj
value = case lookup "doc" obj of
Just v -> case readJSON v of
Ok v' -> v'
Error s -> error s
Nothing -> error $ "toRowDoc: row does not have a value in " ++ show obj
toRowDoc val =
error $ "toRowDoc: expected row to be an object, received " ++ show val
queryView :: (JSON a)
=> String
-> String
-> String
-> [(String, JSValue)]
-> CouchMonad [(JSString, a)]
queryView db viewSet view args = do
let args' = map (\(k,v) -> (k,encode v)) args
let url' = concat [db, "/_design/", viewSet, "/_view/", view]
r <- request url' args' GET [] ""
case rspCode r of
(2,0,0) -> do
let result = couchResponse (rspBody r)
let (JSArray rows) = fromJust $ lookup "rows" result
return $ map toRow rows
otherwise -> error (show r)
queryViewKeys :: String
-> String
-> String
-> [(String, JSValue)]
-> CouchMonad [String]
queryViewKeys db viewSet view args = do
let args' = map (\(k,v) -> (k,encode v)) args
let url' = concat [db, "/_design/", viewSet, "/_view/", view]
r <- request url' args' GET [] ""
case rspCode r of
(2,0,0) -> do
let result = couchResponse (rspBody r)
case lookup "rows" result of
Just (JSArray rows) -> liftIO $ mapM rowKey rows
otherwise -> fail $ "queryView: expected rows"
otherwise -> error (show r)
rowKey :: JSValue -> IO String
rowKey (JSObject obj) = do
let assoc = fromJSObject obj
case lookup "id" assoc of
Just (JSString s) -> return (fromJSString s)
v -> fail "expected id"
rowKey v = fail "expected id"