CouchDB-1.2: src/Database/CouchDB.hs
-- |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