{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, NoMonomorphismRestriction #-} -- | Higher-level functions to interact with CouchDB views. -- -- To automate creation of CouchDB Query Options see -- "Database.CouchDB.Conduit.View.Query" -- -- To manipulate views in design documents see -- "Database.CouchDB.Conduit.Design" module Database.CouchDB.Conduit.View ( -- * Acccessing views #run# -- $run couchView, couchView_, couchViewPost, couchViewPost_, rowValue, rowDoc, rowField, ) where import Control.Exception.Lifted (throw) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BS8 import qualified Data.Text as T import qualified Data.HashMap.Lazy as M import qualified Data.Aeson as A import Data.Attoparsec import qualified Data.Vector.Generic as V import qualified Data.Vector.Fusion.Stream as S import Data.Conduit (MonadResource, Source, Conduit, Sink, ($$), ($=), ($$+-)) import Data.Conduit.Util (sourceState, SourceStateResult(..)) import qualified Data.Conduit.List as CL import qualified Data.Conduit.Attoparsec as CA import qualified Network.HTTP.Conduit as H import qualified Network.HTTP.Types as HT import Database.CouchDB.Conduit.Internal.Connection import Database.CouchDB.Conduit.LowLevel (couch, protect') -- $run -- In contrast to the functions of access to documents that are loaded into -- memory entirely. 'couchView' and 'couchView'' combines the incredible power -- of /http-conduit/ and /attoparsec/ to allow you to process objects in -- constant space. -- -- As data is read from the network, it is fed into attoparsec. When -- attoparsec completes parsing row, it sent to 'Sink'. 'Sink' can be composed -- from many conduits with sink at the end, such as 'rowValue', view conduits -- from "Database.CouchDB.Conduit.Explicit#view" and -- "Database.CouchDB.Conduit.Generic#view", and many others. See -- "Data.Conduit" for details and documentation. -- | Run CouchDB view in manner like 'H.http'. -- -- > runCouch def $ do -- > -- > -- Print all upon receipt. -- > src <- couchView "mydb" "mydesign" "myview" [] -- > src $$ CL.mapM_ (liftIO . print) -- > -- > -- ... Or extract row value and consume -- > src' <- couchView "mydb" "mydesign" "myview" [] -- > res <- src' $= rowValue $$ CL.consume couchView :: MonadCouch m => Path -- ^ Database -> Path -- ^ Design document -> Path -- ^ View name -> HT.Query -- ^ Query parameters -> m (Source m A.Object) couchView db design view q = do H.Response _ _ _ bsrc <- couch HT.methodGet (viewPath db design view) [] q (H.RequestBodyBS B.empty) protect' bsrc $$+- conduitRows -- | Brain-free version of 'couchView'. Takes 'Sink' to consume response. -- -- > runCouch def $ do -- > -- > -- Print all upon receipt. -- > couchView' "mydb" "mydesign" "myview" [] $ CL.mapM_ (liftIO . print) -- > -- > -- ... Or extract row value and consume -- > res <- couchView' "mydb" "mydesign" "myview" [] $ -- > rowValue =$ CL.consume couchView_ :: MonadCouch m => Path -- ^ Database -> Path -- ^ Design document -> Path -- ^ View name -> HT.Query -- ^ Query parameters -> Sink A.Object m a -- ^ Sink for handle view rows. -> m a couchView_ db design view q sink = do raw <- couchView db design view q raw $$ sink -- | Run CouchDB view in manner like 'H.http' using @POST@ (since CouchDB 0.9). -- It's convenient in case that @keys@ paremeter too big for @GET@ query -- string. Other query parameters used as usual. -- -- > runCouch def $ do -- > src <- couchViewPost "mydb" "mydesign" "myview" -- > (mkQuery [QPGroup]) -- > ["key1", "key2", "key3"] -- > src $$ CL.mapM_ (liftIO . print) couchViewPost :: (MonadCouch m, A.ToJSON a) => Path -- ^ Database -> Path -- ^ Design document -> Path -- ^ View name -> HT.Query -- ^ Query parameters -> a -- ^ View @keys@. Must be list or cortege. -> m (Source m A.Object) couchViewPost db design view q ks = do H.Response _ _ _ bsrc <- couch HT.methodPost (viewPath db design view) [] q (H.RequestBodyLBS mkPost) protect' bsrc $$+- conduitRows where mkPost = A.encode $ A.object ["keys" A..= ks] -- | Brain-free version of 'couchViewPost'. Takes 'Sink' to consume response. couchViewPost_ :: (MonadCouch m, A.ToJSON a) => Path -- ^ Database -> Path -- ^ Design document -> Path -- ^ View name -> HT.Query -- ^ Query parameters -> a -- ^ View @keys@. Must be list or cortege. -> Sink A.Object m a -- ^ Sink for handle view rows. -> m a couchViewPost_ db design view q ks sink = do raw <- couchViewPost db design view q ks raw $$ sink -- | Conduit for extract \"value\" field from CouchDB view row. rowValue :: Monad m => Conduit A.Object m A.Value rowValue = rowField "value" -- | Conduit for extract \"doc\" field from CouchDB view row. -- Use only with @include_docs=true@ query parameter. rowDoc :: Monad m => Conduit A.Object m A.Value rowDoc = rowField "doc" -- | Extract field from view row rowField :: Monad m => T.Text -> Conduit A.Object m A.Value rowField f = CL.mapMaybe (M.lookup f) ----------------------------------------------------------------------------- -- Internal ----------------------------------------------------------------------------- -- | Make full view path viewPath :: Path -> Path -> Path -> Path viewPath db design view = mkPath [db, "_design", design, "_view", view] -- | Use an immutable vector as a source. sourceVector :: (Monad m, V.Vector v a) => v a -> Source m a sourceVector vec = sourceState (V.stream vec) f where f stream | S.null stream = return StateClosed | otherwise = return $ StateOpen (S.tail stream) (S.head stream) -- | Extra conduitRows :: MonadResource m => Sink BS8.ByteString m (Source m A.Object) conduitRows = do raw <- CA.sinkParser (A.json "json object") rows <- case raw of (A.Object raw') -> case M.lookup "rows" raw' of (Just (A.Array r)) -> return r _ -> return V.empty _ -> throw $ CouchInternalError "view entry is not an object" return $ sourceVector rows $= CL.map valToObj where valToObj (A.Object o) = o valToObj _ = throw $ CouchInternalError "row is not object"