snaplet-mongoDB-0.2: src/Snap/Snaplet/MongoDB.hs
module Snap.Snaplet.MongoDB
(
MonadMongoDB(..)
, MongoDBSnaplet(..)
, HasMongoDBState(..)
, MongoValue (..)
, MongoEntity (..)
, mongoDBInit
, insert
, insertMany
, insertWith
, get
, getMany
, delete
, deleteWhere
, select
, selectOne
, count
, save
, update
, updateWhere
, filters
, updates
, offset
, limit
, orderAsc
, orderDesc
, objid2bs
, bs2objid
, bs2objid'
, bs2cs
, getObjId
, module Snap.Snaplet.MongoDB.FilterOps
, module Snap.Snaplet.MongoDB.MongoEntity
, module Snap.Snaplet.MongoDB.MongoValue
, module Snap.Snaplet.MongoDB.Template
, module Snap.Snaplet.MongoDB.Parse
, MongoDB.Action
, MongoDB.MonadIO'
) where
import Snap.Core
import Snap.Snaplet
import Prelude hiding (lookup, or)
import Control.Applicative
import Control.Monad
import Control.Monad.MVar
import Control.Monad.Error
import Data.Bson ((=:))
import qualified Data.Bson as BSON
import qualified Data.UString as US
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B8
import qualified Data.CompactString.Internal as CSI
import Data.Maybe (catMaybes, fromJust)
import qualified Data.Map as Map
import Numeric (showHex, readHex)
import Safe
import Snap.Snaplet.MongoDB.FilterOps
import Snap.Snaplet.MongoDB.MongoEntity
import Snap.Snaplet.MongoDB.MongoValue
import Snap.Snaplet.MongoDB.Template
import Snap.Snaplet.MongoDB.Parse
import qualified Database.MongoDB as MongoDB
import qualified Database.MongoDB.Internal.Util as MongoDB
import Database.MongoDB.Query (Action, Failure(..), Database, access, master, AccessMode(..))
import qualified Database.MongoDB.Connection as MongoDB
import qualified System.IO.Pool as MPool
import Control.Monad.CatchIO hiding (Handler)
instance Exception String
------------------------------------------------------------------------------
-- | The 'MonadMongoDB' class. Minimal complete definition:
class MonadIO m => MonadMongoDB m where
----------------------------------------------------------------------------
-- | Run given MongoDB action against the database
withDB :: Action IO a -> m (Either Failure a)
withDBUnsafe :: Action IO a -> m (Either Failure a)
----------------------------------------------------------------------------
-- | Same as 'withDB' but calls 'error' if there is an exception
withDB' :: Action IO a -> m a
withDB' run = do
r <- withDB run
either (throw . show) return r
data MongoDBSnaplet = MongoDBSnaplet {
connPoll :: MPool.Pool IOError MongoDB.Pipe,
appDatabase :: Database
}
------------------------------------------------------------------------------
-- |
class HasMongoDBState s where
getMongoDBState :: Handler s s MongoDBSnaplet
setMongoDBState :: MongoDBSnaplet -> Handler s s ()
--modifyMongoDBState :: (MongoDBSnaplet -> MongoDBSnaplet) -> s -> s
--modifyMongoDBState s = setMongoDBState s getMongoDBState
mongoDBInit :: MongoDB.Host -> Int -> US.UString -> SnapletInit b MongoDBSnaplet
mongoDBInit h n db =
makeSnaplet "mongoDB" "MongoDB abstraction" Nothing $ do
pool <- liftIO $ MPool.newPool (factoryForHost h) n
return $ MongoDBSnaplet pool (db)
where
factoryForHost :: MongoDB.Host -> MPool.Factory IOError MongoDB.Pipe
factoryForHost host = MPool.Factory (newRes h) (killRes) (isResExpired)
newRes :: MongoDB.Host -> ErrorT IOError IO MongoDB.Pipe
newRes = MongoDB.connect
killRes :: MongoDB.Pipe -> IO ()
killRes = MongoDB.close
isResExpired :: MongoDB.Pipe -> IO Bool
isResExpired = MongoDB.isClosed
------------------------------------------------------------------------------
-- |
instance HasMongoDBState s => MonadMongoDB (Handler s s) where
withDB run = do
(MongoDBSnaplet pool db) <- getMongoDBState
epipe <- liftIO $ runErrorT $ MPool.aResource pool
case epipe of
Left err -> return $ Left $ ConnectionFailure err
Right pipe -> do
liftIO (access pipe master db run)
withDBUnsafe run = do
(MongoDBSnaplet pool db) <- getMongoDBState
epipe <- liftIO $ runErrorT $ MPool.aResource pool
case epipe of
Left err -> return $ Left $ ConnectionFailure err
Right pipe -> do
liftIO (access pipe UnconfirmedWrites db run)
-- | Given a type in the type class 'MongoEntity', insert this as a new document in the database. The function will
-- yield the unique key for the new document.
insert :: (MongoDB.MonadIO' m, MongoEntity a) => a -> MongoDB.Action m (Key a)
insert obj = do
(BSON.ObjId objId) <- MongoDB.insert (collectionName obj) (fromDocument $ encodeDocument obj)
pure $! toKey objId
-- | Similar to 'insert', this function inserts multiple documents into the databsae, yielding a unique key for each.
insertMany :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => [a] -> MongoDB.Action m [Key a]
insertMany [] = pure []
insertMany objs = do
ids <- MongoDB.insertMany (collectionName $ head objs) (map (fromDocument . encodeDocument) objs)
forM ids $ \x -> case x of
BSON.ObjId objId -> pure $! toKey objId
_ -> throwError $ MongoDB.QueryFailure 1000 "Expected object ID as result of 'insertMany'"
-- | Similar to 'insert', but allows you to specify the ID with which the object is to be created.
insertWith :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> a -> MongoDB.Action m (Key a)
insertWith key obj = do
(BSON.ObjId objId) <- MongoDB.insert (collectionName obj) (("_id" =: fromKey key) : (fromDocument $ encodeDocument obj))
pure $! toKey objId
-- | Given a unique key for a 'MongoEntity', yield that entity. If no document with the specified ID was found, the
-- function will yield @Nothing@.
get :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> MongoDB.Action m (Maybe a)
get key = do
let collection = collectionName (dummyFromKey key)
result <- MongoDB.findOne (MongoDB.select ["_id" =: fromKey key] collection)
case result of
Just document -> do
eObj <- runErrorT (decodeDocument $ toDocument document)
case eObj of
Left message -> throwError $ MongoDB.QueryFailure 1001 message
Right obj -> pure (Just obj)
Nothing ->
pure Nothing
-- | The application of 'get' to many keys.
getMany :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => [Key a] -> MongoDB.Action m [Maybe a]
getMany = mapM get
-- | Delete the document with the specified key.
delete :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> MongoDB.Action m ()
delete key = do
let collection = collectionName (dummyFromKey key)
MongoDB.deleteOne (MongoDB.select ["_id" =: fromKey key] collection)
deleteWhere :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Document a -> MongoDB.Action m ()
deleteWhere doc = do
MongoDB.delete (MongoDB.select (fromDocument doc) (collectionName (dummyFromDocument doc)))
count :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Document a -> MongoDB.Action m Int
count doc = do
MongoDB.count (MongoDB.select (fromDocument doc) (collectionName (dummyFromDocument doc)))
type SelectorOption = MongoDB.Query -> MongoDB.Query
offset :: Int -> SelectorOption
offset n q = q { MongoDB.skip = fromIntegral n }
limit :: Int -> SelectorOption
limit n q = q { MongoDB.limit = fromIntegral n }
orderAsc :: (MongoEntity a, MongoValue v) => (v -> Filter a) -> SelectorOption
orderAsc f q = q { MongoDB.sort = (filterFieldName (f undefined) =: (1 :: Int)) : MongoDB.sort q }
orderDesc :: (MongoEntity a, MongoValue v) => (v -> Filter a) -> SelectorOption
orderDesc f q = q { MongoDB.sort = (filterFieldName (f undefined) =: ((-1) :: Int)) : MongoDB.sort q }
-- | Select all matching documents from a collection in the database. This function yields a list of pairs of the unique
-- key and the 'MongoEntity' for each matching document in the collection.
--
-- The query can be made using either the quasi-quoted mongoDB query format:
--
-- @select [mongo| { userName: #{name}, userPassword: #{password} |] []@
--
-- Or using the filter operations:
--
-- @select (filters [UserName `eq` name, UserPassword `eq` password]) []@
--
-- /Note/: in the quasi-quotation the names of the fields correspond to the fields stored in the /database/, rather than
-- the fields of the record structure.
--
select :: (Functor m, MonadControlIO m, MongoEntity a) => Document a -> [SelectorOption] -> MongoDB.Action m [(Key a, a)]
select document options = do
cursor <- MongoDB.find (foldr ($) (MongoDB.select (fromDocument document) (collectionName (dummyFromDocument document))) options)
objects <- MongoDB.rest cursor
values <- mapM fetchKeyValue objects
return $ catMaybes values
-- | Similar to 'select', only yielding a single result.
selectOne :: (Functor m, MonadControlIO m, MongoEntity a) => Document a -> [SelectorOption] -> MongoDB.Action m (Maybe (Key a, a))
selectOne document options = do
result <- MongoDB.findOne (foldr ($) (MongoDB.select (fromDocument document) (collectionName (dummyFromDocument document))) options)
case result of
Just obj -> fetchKeyValue obj
Nothing -> pure Nothing
save :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> a -> MongoDB.Action m ()
save key obj = do
MongoDB.save (collectionName obj) (("_id" := BSON.ObjId (fromKey key)) : (fromDocument $ encodeDocument obj))
update :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> Document a -> MongoDB.Action m ()
update key updateDoc = do
MongoDB.modify (MongoDB.select [ "_id" =: (fromKey key) ] (collectionName (dummyFromKey key))) (fromDocument updateDoc)
updateWhere :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Document a -> Document a -> MongoDB.Action m ()
updateWhere document updateDoc = do
MongoDB.modify (MongoDB.select (fromDocument document) (collectionName (dummyFromDocument document))) (fromDocument updateDoc)
filters :: (MongoEntity a) => [FilterOp] -> Document a
filters = toDocument
updates :: (MongoEntity a) => [UpdateOp] -> Document a
updates = toDocument
fetchKeyValue :: (Functor m, MonadControlIO m, MongoEntity a) => BSON.Document -> MongoDB.Action m (Maybe (Key a, a))
fetchKeyValue doc = do
case BSON.look "_id" doc of
Just i ->
case i of
BSON.ObjId objId -> do
eObj <- runErrorT (decodeDocument $ toDocument doc)
case eObj of
Left message -> throwError $ MongoDB.QueryFailure 1001 message
Right obj -> pure $ Just (toKey objId, obj)
_ -> throwError $ MongoDB.QueryFailure 1000 "Expected _id field to be an ObjectId in result of selection"
Nothing ->
throwError $ MongoDB.QueryFailure 1000 "Expected to find an _id field in result of selection"
------------------------------------------------------------------------------
-- | Convert 'ObjectId' into 'ByteString'
objid2bs :: ObjectId -> BS.ByteString
objid2bs (Oid a b) = B8.pack . showHex a . showChar '-' . showHex b $ ""
------------------------------------------------------------------------------
-- | Convert 'ByteString' into 'ObjectId'
bs2objid :: BS.ByteString -> Maybe ObjectId
bs2objid bs = do
case B8.split '-' bs of
(a':b':_) -> do
a <- fmap fst . headMay . readHex . B8.unpack $ a'
b <- fmap fst . headMay . readHex . B8.unpack $ b'
return $ Oid a b
_ -> Nothing
------------------------------------------------------------------------------
-- | Like 'bs2objid', but may blow with an error if the 'ByteString' can't be
-- converted to an 'ObjectId'
bs2objid' :: BS.ByteString -> ObjectId
bs2objid' = fromJust . bs2objid
bs2cs :: BS.ByteString -> US.UString
bs2cs = CSI.CS
------------------------------------------------------------------------------
-- | If the 'Document' has an 'ObjectId' in the given field, return it as
-- 'ByteString'
getObjId :: US.UString -> BSON.Document -> Maybe BS.ByteString
getObjId v d = MongoDB.lookup v d >>= fmap objid2bs
dummyFromKey :: Key a -> a
dummyFromKey _ = undefined
dummyFromDocument :: Document a -> a
dummyFromDocument _ = undefined