-- | an unsafe interface to CouchDB. Database and document names are not -- sanitized. module Database.CouchDB.Unsafe ( -- * Databases createDB , dropDB , getAllDBs -- * Documents , newNamedDoc , newDoc , updateDoc , bulkUpdateDocs , deleteDoc , forceDeleteDoc , getDocPrim , getDocRaw , getDoc , getAndUpdateDoc , getAllDocIds , getAllDocs -- * Views -- $views , 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 [] "" -- |Creates a new database. Throws an exception if the database already -- exists. createDB :: String -> CouchMonad () createDB name = do resp <- request' name PUT unless (rspCode resp == (2,0,1)) $ error (rspReason resp) dropDB :: String -> CouchMonad Bool -- ^False if the database does not exist 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 -- ^database name -> String -- ^document name -> a -- ^document body -> CouchMonad (Either String JSString) -- ^Returns 'Left' on a conflict. Returns 'Right' with the -- revision number on success. 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 -- ^database -> (JSString,JSString) -- ^document and revision -> a -- ^ new value -> 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 -- ^database -> [a] -- ^ all docs -> CouchMonad (Maybe [Either JSString (JSString, JSString)]) -- ^ error or (id,rev) 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 -- |Delete a doc by document identifier (revision number not needed). This -- operation first retreives the document to get its revision number. It fails -- if the document doesn't exist or there is a conflict. forceDeleteDoc :: String -- ^ database -> String -- ^ document identifier -> 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 -- ^database -> (JSString,JSString) -- ^document and revision -> 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 -- TODO: figure out which error codes are normal (delete conflicts) otherwise -> fail $ "deleteDoc failed: " ++ (show r) newDoc :: (JSON a) => String -- ^database name -> a -- ^document body -> CouchMonad (JSString,JSString) -- ^ id and rev of new document 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 -- ^database name -> String -- ^document name -> CouchMonad (Maybe (JSString,JSString,a)) -- ^'Nothing' if the -- doc does not exist 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 -- doc does not exist otherwise -> error (show r) -- |Gets a document as a raw JSON value. Returns the document id, -- revision and value as a 'JSObject'. These fields are queried lazily, -- and may fail later if the response from the server is malformed. getDocPrim :: String -- ^database name -> String -- ^document name -> CouchMonad (Maybe (JSString,JSString,[(String,JSValue)])) -- ^'Nothing' if the document does not exist. 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 -- doc does not exist code -> fail $ "getDocPrim: " ++ show code ++ " error" -- |Gets a document as a Maybe String. Returns the raw result of what -- couchdb returns. Returns Nothing if the doc does not exist. 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 -- doc does not exist code -> fail $ "getDocRaw: " ++ show code ++ " error" getAndUpdateDoc :: (JSON a) => String -- ^database -> String -- ^document name -> (a -> IO a) -- ^update function -> CouchMonad (Maybe String) -- ^If the update succeeds, -- return the revision number -- of the result. 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 -- ^database name -> 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) -- -- $views -- Creating and querying views -- 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 -- ^database name -> String -- ^view set name -> [CouchView] -- ^views -> 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 -- ^databse -> [(String, JSValue)] -- ^query parameters -- |Returns a list of rows. Each row is a key, value pair. -> 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 -- ^database -> String -- ^design -> String -- ^view -> [(String, JSValue)] -- ^query parameters -- |Returns a list of rows. Each row is a key, value pair. -> 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) -- |Like 'queryView', but only returns the keys. Use this for key-only -- views where the value is completely ignored. queryViewKeys :: String -- ^database -> String -- ^design -> String -- ^view -> [(String, JSValue)] -- ^query parameters -> 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"