{-# LANGUAGE OverloadedStrings, TemplateHaskell, DeriveDataTypeable, ScopedTypeVariables, FlexibleInstances #-} module Database.Cypher ( Cypher, Entity(..), CypherResult(..), LuceneQuery, runCypher, cypher, cypherGetNode, cypherCreate, cypherGet, cypherSet, luceneEncode, withCypherManager, CypherException(..), DBInfo(..), Hostname, Port, CypherVal(..), CypherVals(..), CypherCol(..), CypherCols(..), CypherMaybe(..), CypherUnit(..) ) where import Database.Cypher.Lucene import Data.Aeson import Data.Aeson.TH import Data.Aeson.Types import Network.HTTP.Conduit import Network.HTTP.Types import Data.Conduit import Data.Typeable import Data.Text (Text) import Control.Exception import Control.Applicative import Control.Monad import Data.Monoid import Control.Monad.IO.Class import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import Data.Text.Lazy.Encoding (encodeUtf8) import qualified Data.HashMap.Strict as H import Data.Text.Lazy.Builder import Data.Aeson.Encode -- | Information about your neo4j configuration needed to make requests over the REST api. data DBInfo = DBInfo { cypher_hostname :: Hostname, cypher_port :: Port } deriving (Show, Eq) type Hostname = S.ByteString type Port = Int $(deriveJSON (drop 7) ''DBInfo) -- | All interaction with Neo4j is done through the Cypher monad. Use 'cypher' to add a query to the monad. newtype Cypher a = Cypher { uncypher :: ((DBInfo, Manager) -> ResourceT IO a) } -- | Raw result data returned by Neo4j. Only use this if you care about column headers. data CypherResult a = CypherResult { rescolumns :: [Text], resdata :: a } deriving (Show, Eq) -- | A single result returned by Neo4j. newtype CypherVal a = CypherVal a deriving (Eq, Show) -- | A single column returned by Neo4j. newtype CypherCol a = CypherCol a deriving (Eq, Show) -- | Columns returned by Neo4j. newtype CypherCols a = CypherCols a deriving (Eq, Show) -- | Values returned by Neo4j. newtype CypherVals a = CypherVals [a] deriving (Eq, Show) -- | Possibly a value returned by Neo4j data CypherMaybe a = CypherJust a | CypherNothing deriving (Eq, Show) -- | No value returned from Neo4j data CypherUnit = CypherUnit deriving (Show) data CypherRequest = CypherRequest { req_query :: Text, req_params :: Value } deriving (Show, Eq) -- | A neo4j node or edge data Entity a = Entity { entity_id :: String, entity_properties :: String, entity_data :: a } deriving (Show, Eq) instance FromJSON a => FromJSON (Entity a) where parseJSON (Object v) = Entity <$> v .: "self" <*> v .: "properties" <*> v .: "data" parseJSON _ = mempty instance ToJSON (Entity a) where toJSON = toJSON . entity_id $(deriveJSON (drop 3) ''CypherResult) $(deriveJSON (drop 4) ''CypherRequest) -- | An error in handling a Cypher query, either in communicating with the server or parsing the result data CypherException = CypherServerException Status ResponseHeaders L.ByteString | CypherClientParseException S.ByteString deriving (Show, Typeable) instance Exception CypherException throwClientParse bs = throw $ CypherClientParseException $ S.concat $ L.toChunks bs instance Monad Cypher where return a = Cypher (const (return a)) (Cypher cmd) >>= f = Cypher $ \con-> do a <- cmd con uncypher (f a) con instance MonadIO Cypher where liftIO f = Cypher $ const (liftIO f) instance FromJSON a => FromJSON (CypherVal a) where parseJSON x = do (CypherResult _ [[d]]) <- parseJSON x return $ CypherVal d instance FromJSON a => FromJSON (CypherCol a) where parseJSON x = do (CypherResult _ [d]) <- parseJSON x return $ CypherCol d instance FromJSON a => FromJSON (CypherCols a) where parseJSON x = do (CypherResult _ d) <- parseJSON x return $ CypherCols d instance FromJSON a => FromJSON (CypherVals a) where parseJSON x = do (CypherResult _ d) <- parseJSON x liftM CypherVals (mapM safeHead d) instance FromJSON a => FromJSON (CypherMaybe a) where parseJSON x = do (CypherResult _ ds) <- parseJSON x case ds of [[d]] -> return $ CypherJust d _ -> return CypherNothing instance FromJSON CypherUnit where parseJSON _ = return CypherUnit safeHead :: [a] -> Parser a safeHead [a] = return a safeHead _ = mzero -- | Perform a cypher query cypher :: FromJSON a => Text -> Value -> Cypher a cypher txt params = Cypher $ \(DBInfo h p, m)-> do let req = def { host = h, port = p, path = "db/data/cypher", requestBody = RequestBodyLBS (encode $ CypherRequest txt params), requestHeaders = headerAccept "application/json" : headerContentType "application/json" : requestHeaders def, method = "POST", checkStatus = (\_ _-> Nothing) } r <- httpLbs req m let sci = statusCode (responseStatus r) if 200 <= sci && sci < 300 then (case decode (responseBody r) of Nothing -> throwClientParse (responseBody r) Just x-> return x) else throw $ CypherServerException (responseStatus r) (responseHeaders r) (responseBody r) -- | Create a cypher node cypherCreate :: (ToJSON a, FromJSON b) => a -> Cypher b cypherCreate obj = Cypher $ \(DBInfo h p, m)-> do let req = def { host = h, port = p, path = "db/data/node", requestBody = RequestBodyLBS (encode obj), requestHeaders = headerAccept "application/json" : headerContentType "application/json" : requestHeaders def, method = "POST", checkStatus = (\_ _-> Nothing) } r <- httpLbs req m let sci = statusCode (responseStatus r) if 200 <= sci && sci < 300 then (case decode (responseBody r) of Nothing -> throwClientParse (responseBody r) Just x-> return x) else throw $ CypherServerException (responseStatus r) (responseHeaders r) (responseBody r) -- | Get a cypher node cypherGetNode :: FromJSON b => Entity b -> Cypher (Entity b) cypherGetNode e = Cypher $ \(DBInfo h p, m)-> do req <- liftIO $ parseUrl (entity_id e) let req' = req { host = h, port = p, requestHeaders = headerAccept "application/json" : headerContentType "application/json" : requestHeaders def, method = "GET", checkStatus = (\_ _-> Nothing) } r <- httpLbs req m let sci = statusCode (responseStatus r) if 200 <= sci && sci < 300 then (case decode (responseBody r) of Nothing -> throwClientParse (responseBody r) Just x-> return x) else throw $ CypherServerException (responseStatus r) (responseHeaders r) (responseBody r) -- | Set cypher properties. This currently cannot be done through cypher queries. cypherSet :: (ToJSON a, ToJSON a1) => (Entity a) -> a1 -> Cypher () cypherSet e obj = Cypher $ \(DBInfo h p, m)-> do let Object o1 = toJSON (entity_data e) let Object o2 = toJSON obj let body = RequestBodyLBS $ encodeUtf8 $ toLazyText $ fromValue $ Object (o2 `H.union` o1) req <- liftIO $ parseUrl (entity_properties e) let req' = req { host = h, port = p, requestBody = body, requestHeaders = headerAccept "application/json" : headerContentType "application/json" : requestHeaders def, method = "PUT", checkStatus = (\_ _-> Nothing) } r <- httpLbs req' m let sci = statusCode (responseStatus r) if 200 <= sci && sci < 300 then return () else (let e = CypherServerException (responseStatus r) (responseHeaders r) (responseBody r) in traceShow e (throw e)) -- | Get the nodes matching the given lucene query cypherGet :: (ToJSON a1, FromJSON a) => a1 -> Cypher a cypherGet lc = cypher "start a = node:node_auto_index({lc}) return a" $ object ["lc" .= lc] -- | Get the http connection manager for a Cypher monad withCypherManager :: (Manager -> ResourceT IO a) -> Cypher a withCypherManager f = Cypher (\(_,m)-> f m) -- | Execute some number of cypher queries runCypher :: Cypher a -> DBInfo -> Manager -> IO a runCypher c dbi m = runResourceT $ do uncypher c (dbi, m)