-- |Interface to CouchDB. module Database.CouchDB ( -- * Initialization CouchMonad , runCouchDB , runCouchDB' , runCouchDBURI -- * Explicit Connections , CouchConn() , runCouchDBWith , createCouchConn , createCouchConnFromURI , closeCouchConn -- * Databases , DB , db , isDBString , createDB , dropDB , getAllDBs -- * Documents , Doc , Rev , doc , rev , isDocString , newNamedDoc , newDoc , updateDoc , bulkUpdateDocs , deleteDoc , forceDeleteDoc , getDocPrim , getDocRaw , getDoc , getAllDocs , getAndUpdateDoc , getAllDocIds -- * Views -- $views , CouchView (..) , newView , queryView , queryViewKeys ) where import Database.CouchDB.HTTP import Control.Monad import Control.Monad.Trans (liftIO) import Data.Maybe (fromJust,mapMaybe,maybeToList) import Text.JSON import Data.List (elem) import Data.Maybe (mapMaybe) import Database.CouchDB.Unsafe (CouchView (..)) import qualified Data.List as L import qualified Database.CouchDB.Unsafe as U -- |Database name data DB = DB String instance Show DB where show (DB s) = s instance JSON DB where readJSON val = do s <- readJSON val case isDBString s of False -> fail "readJSON: not a valid database name" True -> return (DB s) showJSON (DB s) = showJSON s isDBFirstChar ch = (ch >= 'a' && ch <= 'z') isDBOtherChar ch = (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch `elem` "_$()+-/" -- Pretty much anything is accepted in document IDs, but avoid the -- initial '_' as it is reserved. It is likely possible to accept -- more, but this includes at least the auto-generated IDs. isFirstDocChar ch = (ch >= 'A' && ch <='Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch `elem` "-@." isDocChar ch = (ch >= 'A' && ch <='Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch `elem` "-@._" isDBString :: String -> Bool isDBString [] = False isDBString (first:[]) = isDBFirstChar first isDBString (first:rest) = isDBFirstChar first && and (map isDBOtherChar rest) -- |Returns a safe database name. Signals an error if the name is -- invalid. db :: String -> DB db dbName = case isDBString dbName of True -> DB dbName False -> error $ "db : invalid dbName (" ++ dbName ++ ")" -- |Document revision number. data Rev = Rev { unRev :: JSString } deriving (Eq,Ord) instance Show Rev where show (Rev s) = fromJSString s -- |Document name data Doc = Doc { unDoc :: JSString } deriving (Eq,Ord) instance Show Doc where show (Doc s) = fromJSString s instance JSON Doc where readJSON (JSString s) | isDocString (fromJSString s) = return (Doc s) readJSON _ = fail "readJSON: not a valid document name" showJSON (Doc s) = showJSON s instance Read Doc where readsPrec _ str = maybeToList (parseFirst str) where parseFirst "" = Nothing parseFirst (ch:rest) | isFirstDocChar ch = let (chs',rest') = parseRest rest in Just (Doc $ toJSString $ ch:chs',rest) | otherwise = Nothing parseRest "" = ("","") parseRest (ch:rest) | isDocChar ch = let (chs',rest') = parseRest rest in (ch:chs',rest') | otherwise = ("",ch:rest) -- |Returns a Rev rev :: String -> Rev rev = Rev . toJSString -- |Returns a safe document name. Signals an error if the name is -- invalid. doc :: String -> Doc doc docName = case isDocString docName of True -> Doc (toJSString docName) False -> error $ "doc : invalid docName (" ++ docName ++ ")" isDocString :: String -> Bool isDocString [] = False isDocString (first:rest) = isFirstDocChar first && and (map isDocChar rest) -- |Creates a new database. Throws an exception if the database already -- exists. createDB :: String -> CouchMonad () createDB = U.createDB dropDB :: String -> CouchMonad Bool -- ^False if the database does not exist dropDB = U.dropDB getAllDBs :: CouchMonad [DB] getAllDBs = U.getAllDBs >>= \dbs -> return [db $ fromJSString s | s <- dbs] newNamedDoc :: (JSON a) => DB -- ^database name -> Doc -- ^document name -> a -- ^document body -> CouchMonad (Either String Rev) -- ^Returns 'Left' on a conflict. newNamedDoc dbName docName body = do r <- U.newNamedDoc (show dbName) (show docName) body case r of Left s -> return (Left s) Right rev -> return (Right $ Rev rev) updateDoc :: (JSON a) => DB -- ^database -> (Doc,Rev) -- ^document and revision -> a -- ^ new value -> CouchMonad (Maybe (Doc,Rev)) updateDoc db (doc,rev) val = do r <- U.updateDoc (show db) (unDoc doc, unRev rev) val case r of Nothing -> return Nothing Just (_,rev) -> return $ Just (doc,Rev rev) bulkUpdateDocs :: (JSON a) => DB -- ^database -> [a] -- ^ new docs -> CouchMonad (Maybe [Either String (Doc, Rev)]) bulkUpdateDocs db docs = do r <- U.bulkUpdateDocs (show db) docs case r of Nothing -> return Nothing Just es -> return $ Just $ map (\e -> case e of Left err -> Left $ fromJSString err Right (doc, rev) -> Right (Doc doc, Rev rev) ) es -- |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 :: DB -- ^ database -> Doc -- ^ document identifier -> CouchMonad Bool forceDeleteDoc db doc = U.forceDeleteDoc (show db) (show doc) deleteDoc :: DB -- ^database -> (Doc,Rev) -> CouchMonad Bool deleteDoc db (doc,rev) = U.deleteDoc (show db) (unDoc doc,unRev rev) newDoc :: (JSON a) => DB -- ^database name -> a -- ^document body -> CouchMonad (Doc,Rev) -- ^ id and rev of new document newDoc db body = do (doc,rev) <- U.newDoc (show db) body return (Doc doc,Rev rev) getDoc :: (JSON a) => DB -- ^database name -> Doc -- ^document name -> CouchMonad (Maybe (Doc,Rev,a)) -- ^'Nothing' if the -- doc does not exist getDoc db doc = do r <- U.getDoc (show db) (show doc) case r of Nothing -> return Nothing Just (_,rev,val) -> return $ Just (doc,Rev rev,val) getAllDocs :: JSON a => DB -> [(String, JSValue)] -- ^query parameters -> CouchMonad [(Doc, a)] getAllDocs db args = do rows <- U.getAllDocs (show db) args return $ map (\(doc,val) -> (Doc doc,val)) rows -- |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 :: DB -- ^database name -> Doc -- ^document name -> CouchMonad (Maybe (Doc,Rev,[(String,JSValue)])) -- ^'Nothing' if the document does not exist. getDocPrim db doc = do r <- U.getDocPrim (show db) (show doc) case r of Nothing -> return Nothing Just (_,rev,obj) -> return $ Just (doc,Rev rev,obj) getDocRaw :: DB -> Doc -> CouchMonad (Maybe String) getDocRaw db doc = U.getDocRaw (show db) (show doc) getAndUpdateDoc :: (JSON a) => DB -- ^database -> Doc -- ^document name -> (a -> IO a) -- ^update function -> CouchMonad (Maybe Rev) -- ^If the update succeeds, -- return the revision number -- of the result. getAndUpdateDoc db docId fn = do r <- U.getAndUpdateDoc (show db) (show docId) fn case r of Nothing -> return Nothing Just rev -> return $ Just (Rev $ toJSString rev) getAllDocIds ::DB -- ^database name -> CouchMonad [Doc] getAllDocIds db = do allIds <- U.getAllDocIds (show db) return (map Doc allIds) -- -- $views -- Creating and querying views -- newView :: String -- ^database name -> String -- ^view set name -> [CouchView] -- ^views -> CouchMonad () newView = U.newView queryView :: (JSON a) => DB -- ^database -> Doc -- ^design -> Doc -- ^view -> [(String, JSValue)] -- ^query parameters -- |Returns a list of rows. Each row is a key, value pair. -> CouchMonad [(Doc, a)] queryView db viewSet view args = do rows <- U.queryView (show db) (show viewSet) (show view) args return $ map (\(doc,val) -> (Doc doc,val)) rows -- |Like 'queryView', but only returns the keys. Use this for key-only -- views where the value is completely ignored. queryViewKeys :: DB -- ^database -> Doc -- ^design -> Doc -- ^view -> [(String, JSValue)] -- ^query parameters -> CouchMonad [Doc] queryViewKeys db viewSet view args = do rows <- U.queryViewKeys (show db) (show viewSet) (show view) args return $ map (Doc . toJSString) rows