haskell-neo4j-client 0.1.0.0 → 0.1.5.0
raw patch · 6 files changed
+309/−6 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Database.Neo4j.Cypher: ParamLiteral :: PropertyValue -> ParamValue
+ Database.Neo4j.Cypher: ParamProperties :: Properties -> ParamValue
+ Database.Neo4j.Cypher: ParamPropertiesArray :: [Properties] -> ParamValue
+ Database.Neo4j.Cypher: Response :: [Text] -> [[Value]] -> Response
+ Database.Neo4j.Cypher: cols :: Response -> [Text]
+ Database.Neo4j.Cypher: cypher :: Text -> Params -> Neo4j (Either Text Response)
+ Database.Neo4j.Cypher: data ParamValue
+ Database.Neo4j.Cypher: data Response
+ Database.Neo4j.Cypher: fromResult :: Response -> Either Text Response -> Response
+ Database.Neo4j.Cypher: fromSuccess :: Either Text Response -> Response
+ Database.Neo4j.Cypher: instance Eq ParamValue
+ Database.Neo4j.Cypher: instance Eq Response
+ Database.Neo4j.Cypher: instance FromJSON Response
+ Database.Neo4j.Cypher: instance Show ParamValue
+ Database.Neo4j.Cypher: instance Show Response
+ Database.Neo4j.Cypher: instance ToJSON ParamValue
+ Database.Neo4j.Cypher: isSuccess :: Either Text Response -> Bool
+ Database.Neo4j.Cypher: newparam :: PropertyValueConstructor a => a -> ParamValue
+ Database.Neo4j.Cypher: type Params = HashMap Text ParamValue
+ Database.Neo4j.Cypher: vals :: Response -> [[Value]]
+ Database.Neo4j.Graph: addCypher :: Response -> Graph -> Graph
Files
- haskell-neo4j-client.cabal +3/−3
- src/Database/Neo4j.hs +20/−0
- src/Database/Neo4j/Cypher.hs +88/−0
- src/Database/Neo4j/Graph.hs +19/−0
- src/Database/Neo4j/Http.hs +4/−2
- tests/IntegrationTests.hs +175/−1
haskell-neo4j-client.cabal view
@@ -1,11 +1,11 @@ name: haskell-neo4j-client-version: 0.1.0.0+version: 0.1.5.0 synopsis: A Haskell neo4j client description: Library to interact with Neo4j databases. For now, its API covers basic operations for nodes, relationships, labels and indexes and provides calls to use these in batch mode. .- Cypher is not yet supported, but it hopefully will be very soon.+ Basic support for Cypher implemented . All code has been tested with Neo4j version 2.0.3 homepage: https://github.com/asilvestre/haskell-neo4j-rest-client@@ -47,7 +47,7 @@ mtl ==2.2.* library hs-source-dirs: src- exposed-modules: Database.Neo4j, Database.Neo4j.Graph, Database.Neo4j.Batch+ exposed-modules: Database.Neo4j, Database.Neo4j.Graph, Database.Neo4j.Batch, Database.Neo4j.Cypher other-modules: Database.Neo4j.Types, Database.Neo4j.Http, Database.Neo4j.Node, Database.Neo4j.Relationship, Database.Neo4j.Property, Database.Neo4j.Label, Database.Neo4j.Index, Database.Neo4j.Batch.Node, Database.Neo4j.Batch.Relationship, Database.Neo4j.Batch.Property, Database.Neo4j.Types,
src/Database/Neo4j.hs view
@@ -107,3 +107,23 @@ -- instance 'Neo4jNoEntityException', or more generic ones like 'Neo4jHttpException' or 'Neo4jParseException' -- if the server returns something totally unexpected. (I'm sure there's still work to do here preparing the code -- to return more specific exceptions for known scenarios)+-- +-- About Cypher support for now we allow sending queries with parameters, the result is a collection of column headers+-- and JSON data values, the Graph object has the function addCypher that tries to find+-- nodes and relationships in a cypher query result and insert them in a "Database.Neo4j.Graph" object+--+-- > import qualified Database.Neo4j.Cypher as C+-- >+-- > withConnection host port $ do+-- > ...+-- > -- Run a cypher query with parameters+-- > res <- C.cypher "CREATE (n:Person { name : {name} }) RETURN n" M.fromList [("name", C.newparam ("Pep" :: T.Text))]+-- >+-- > -- Get all nodes and relationships that this query returned and insert them in a Graph object+-- > let graph = G.addCypher (C.fromSuccess res) G.empty+-- >+-- > -- Get the column headers+-- > let columnHeaders = C.cols $ C.fromSuccess res+-- >+-- > -- Get the rows of JSON values received+-- > let values = C.vals $ C.fromSuccess res
+ src/Database/Neo4j/Cypher.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Module to provide Cypher support.+-- Currently we allow sending queries with parameters, the result is a collection of column headers+-- and JSON data values, the Graph object has the function addCypher that tries to find+-- nodes and relationships in a cypher query result and insert them in a "Database.Neo4j.Graph" object+--+-- > import qualified Database.Neo4j.Cypher as C+-- >+-- > withConnection host port $ do+-- > ...+-- > -- Run a cypher query with parameters+-- > res <- C.cypher "CREATE (n:Person { name : {name} }) RETURN n" M.fromList [("name", C.newparam ("Pep" :: T.Text))]+-- >+-- > -- Get all nodes and relationships that this query returned and insert them in a Graph object+-- > let graph = G.addCypher (C.fromSuccess res) G.empty+-- >+-- > -- Get the column headers+-- > let columnHeaders = C.cols $ C.fromSuccess res+-- >+-- > -- Get the rows of JSON values received+-- > let values = C.vals $ C.fromSuccess res+module Database.Neo4j.Cypher (+ -- * Types+ Response(..), ParamValue(..), Params, newparam,+ -- * Sending queries+ cypher, fromResult, fromSuccess, isSuccess+ ) where++import Data.Aeson ((.=), (.:))+import Control.Applicative ((<$>), (<*>))+import Control.Monad (mzero)++import qualified Data.Aeson as J+import qualified Data.ByteString as S+import qualified Data.HashMap.Lazy as M+import qualified Data.Text as T++import Database.Neo4j.Types+import Database.Neo4j.Http++-- | Type for a Cypher response with tuples containing column name and their values+data Response = Response {cols :: [T.Text], vals :: [[J.Value]]} deriving (Show, Eq)++-- | How to create a response object from a cypher JSON response+instance J.FromJSON Response where+ parseJSON (J.Object o) = Response <$> (o .: "columns" >>= J.parseJSON) <*> (o .: "data" >>= J.parseJSON)+ parseJSON _ = mzero+ +cypherAPI :: S.ByteString+cypherAPI = "/db/data/cypher"++-- | Value for a cypher parmeter value, might be a literal, a property map or a list of property maps+data ParamValue = ParamLiteral PropertyValue | ParamProperties Properties | ParamPropertiesArray [Properties]+ deriving (Show, Eq)++newparam :: PropertyValueConstructor a => a -> ParamValue+newparam = ParamLiteral . newval++-- | Instance toJSON for param values so we can serialize them in queries+instance J.ToJSON ParamValue where+ toJSON (ParamLiteral l) = J.toJSON l+ toJSON (ParamProperties p) = J.toJSON p+ toJSON (ParamPropertiesArray ps) = J.toJSON ps++-- | We use hashmaps to represent Cypher parameters+type Params = M.HashMap T.Text ParamValue++-- | Run a cypher query+cypher :: T.Text -> Params -> Neo4j (Either T.Text Response)+cypher cmd params = Neo4j $ \conn -> httpCreate4XXExplained conn cypherAPI body+ where body = J.encode $ J.object ["query" .= cmd, "params" .= J.toJSON params]++-- | Get the result of the response or a default value+fromResult :: Response -> Either T.Text Response -> Response+fromResult def (Left _) = def+fromResult _ (Right resp) = resp++-- | Get the result of the response or a default value+fromSuccess :: Either T.Text Response -> Response+fromSuccess (Left _) = error "Cypher.fromSuccess but is Error"+fromSuccess (Right resp) = resp++-- | True if the operation succeeded+isSuccess :: Either T.Text Response -> Bool+isSuccess (Left _) = False+isSuccess (Right _) = True+
src/Database/Neo4j/Graph.hs view
@@ -21,6 +21,8 @@ setProperties, setProperty, deleteProperties, deleteProperty, -- * Handling labels setNodeLabels, addNodeLabel, getNodeLabels, deleteNodeLabel,+ -- * Handling Cypher results+ addCypher, -- * Graph filtering functions nodeFilter, relationshipFilter, -- * Graph operations@@ -29,11 +31,15 @@ import Data.Maybe (fromMaybe) +import Control.Applicative ((<$>), (<*>))+import qualified Data.Aeson as J import qualified Data.HashMap.Lazy as M import qualified Data.HashSet as HS+import qualified Data.List as L import qualified Data.Text as T import Database.Neo4j.Types+import qualified Database.Neo4j.Cypher as C type NodeIndex = M.HashMap NodePath Node type RelIndex = M.HashMap RelPath Relationship@@ -213,3 +219,16 @@ intersection ga gb = relationshipFilter relFilterFunc (nodeFilter nodeFilterFunc ga) where relFilterFunc r = hasRelationship r gb nodeFilterFunc n = hasNode n gb++-- | Feed a cypher result into a graph (looks for nodes and relationships and inserts them)+addCypher :: C.Response -> Graph -> Graph+addCypher (C.Response _ vals) ginit = foldl tryAdd ginit (concat vals)+ where tryAdd :: Graph -> J.Value -> Graph+ -- Try first to parse it as a relationship and then as a node, otherwise leave the graph as it is+ tryAdd g v = fromMaybe g $ L.find parseSuccess (map ($ v) [relParser g, nodeParser g]) >>= fromResult+ relParser g v = flip addRelationship g <$> J.fromJSON v+ nodeParser g v = flip addNode g <$> J.fromJSON v+ parseSuccess (J.Success _) = True+ parseSuccess (J.Error _) = False+ fromResult (J.Error _) = Nothing+ fromResult (J.Success g) = Just g
src/Database/Neo4j/Http.hs view
@@ -85,12 +85,14 @@ -- With 404 or 400 will return a left with the explanation httpCreate4XXExplained :: J.FromJSON a => Connection -> S.ByteString -> L.ByteString -> ResourceT IO (Either T.Text a) httpCreate4XXExplained conn path body = do- res <- httpReq conn HT.methodPost path body (\s -> s `elem` [HT.status201, HT.status404, HT.status400])+ res <- httpReq conn HT.methodPost path body (\s -> s `elem` validcodes ++ errcodes) let status = HC.responseStatus res- return $ if status == HT.status201 then parseBody res else Left $ extractException res+ return $ if status `elem` validcodes then parseBody res else Left $ extractException res where parseBody resp = case J.eitherDecode $ HC.responseBody resp of Right entity -> Right entity Left e -> throw $ Neo4jParseException ("Error parsing created entity: " ++ e)+ validcodes = [HT.status200, HT.status201]+ errcodes = [HT.status404, HT.status400] -- | Launch a POST request that doesn't expect response body, this will raise an exception if 204 is not received httpCreate_ :: Connection -> S.ByteString -> L.ByteString -> ResourceT IO ()
tests/IntegrationTests.hs view
@@ -7,11 +7,13 @@ import Data.Int (Int64) import Data.Maybe (fromJust) +import qualified Data.Aeson as J import qualified Data.ByteString as S import qualified Data.HashMap.Lazy as M import qualified Data.HashSet as HS import qualified Data.Text as T import qualified Data.Text.Encoding as TE+import qualified Data.Vector as V import Control.Exception (handle) import Test.Framework.TH (defaultMainGenerator)@@ -19,8 +21,9 @@ import Test.HUnit.Base hiding (Test, Node) import Database.Neo4j-import qualified Database.Neo4j.Graph as G import qualified Database.Neo4j.Batch as B+import qualified Database.Neo4j.Cypher as C+import qualified Database.Neo4j.Graph as G (<>) :: Monoid a => a -> a -> a (<>) = mappend@@ -1175,3 +1178,174 @@ return () where lbl1 = "lbl1" lbl2 = "lbl2"++-- | Test cypher basic test+case_cypherBasic :: Assertion+case_cypherBasic = withConnection host port $ do+ -- create initial data+ gp <- B.runBatch $ do+ n1 <- B.createNode $ M.fromList ["name" |: ("I" :: T.Text)]+ n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)]+ B.createRelationship "know" M.empty n1 n2+ -- actual test+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ neo4jEqual ["TYPE(r)"] (C.cols $ C.fromSuccess res)+ neo4jBool $ ["know"] `elem` (C.vals $ C.fromSuccess res)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp)+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "MATCH (x {name: {startName}})-[r]-(friend) WHERE friend.name = {name} RETURN TYPE(r)"+ params = M.fromList [("startName", C.newparam ("I" :: T.Text)), ("name", C.newparam ("you" :: T.Text))]+++-- | Test cypher create a node+case_cypherCreateNode :: Assertion+case_cypherCreateNode = withConnection host port $ do+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ let gp = G.addCypher (C.fromSuccess res) G.empty+ let props = M.fromList ["name" |: ("Andres" :: T.Text)]+ neo4jEqual 1 (length $ G.getNodes gp)+ neo4jBool $ props `elem` map getNodeProperties (G.getNodes gp)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "CREATE (n:Person { name : {name} }) RETURN n"+ params = M.fromList [("name", C.newparam ("Andres" :: T.Text))]++-- | Test cypher create a node with multiple properties+case_cypherCreateNodeMultipleProperties :: Assertion+case_cypherCreateNodeMultipleProperties = withConnection host port $ do+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ let gp = G.addCypher (C.fromSuccess res) G.empty+ neo4jEqual 1 (length $ G.getNodes gp)+ neo4jBool $ props `elem` map getNodeProperties (G.getNodes gp)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "CREATE (n:Person { props }) RETURN n"+ props = M.fromList ["name" |: ("Michael" :: T.Text), "position" |: ("Developer" :: T.Text),+ "awesome" |: True, "children" |: (3 :: Int64)]+ params = M.fromList [("props", C.ParamProperties props)]++-- | Test cypher create multiple nodes+case_cypherCreateMultipleNodes :: Assertion+case_cypherCreateMultipleNodes = withConnection host port $ do+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ let gp = G.addCypher (C.fromSuccess res) G.empty+ neo4jEqual 2 (length $ G.getNodes gp)+ neo4jBool $ propsA `elem` map getNodeProperties (G.getNodes gp)+ neo4jBool $ propsB `elem` map getNodeProperties (G.getNodes gp)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "CREATE (n:Person { props }) RETURN n"+ propsA = M.fromList ["name" |: ("Andres" :: T.Text), "position" |: ("Developer" :: T.Text)]+ propsB = M.fromList ["name" |: ("Michael" :: T.Text), "position" |: ("Developer" :: T.Text)]+ params = M.fromList [("props", C.ParamPropertiesArray [propsA, propsB])]++-- | Test cypher set all properties on a node+case_cypherSetAllNodeProperties :: Assertion+case_cypherSetAllNodeProperties = withConnection host port $ do+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ let gp = G.addCypher (C.fromSuccess res) G.empty+ neo4jEqual 1 (length $ G.getNodes gp)+ neo4jBool $ props `elem` map getNodeProperties (G.getNodes gp)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "CREATE (n:Person { name: 'this property is to be deleted' } ) SET n = { props } RETURN n"+ props = M.fromList ["name" |: ("Michael" :: T.Text), "position" |: ("Developer" :: T.Text),+ "awesome" |: True, "children" |: (3 :: Int64)]+ params = M.fromList [("props", C.ParamProperties props)]++-- | Test cypher basic query+case_cypherBasicQuery :: Assertion+case_cypherBasicQuery = withConnection host port $ do+ -- create initial data+ gp <- B.runBatch $ do+ n1 <- B.createNode $ M.fromList ["name" |: ("I" :: T.Text)]+ n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)]+ n3 <- B.createNode $ M.fromList ["name" |: ("him" :: T.Text), "age" |: (25 :: Int64)]+ _ <- B.createRelationship "know" M.empty n1 n2+ B.createRelationship "know" M.empty n1 n3+ -- actual test+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ neo4jEqual ["type(r)", "n.name", "n.age"] (C.cols $ C.fromSuccess res)+ neo4jBool $ ["know", "him", J.Number 25] `elem` (C.vals $ C.fromSuccess res)+ neo4jBool $ ["know", "you", J.Null] `elem` (C.vals $ C.fromSuccess res)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp)+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "MATCH (x {name: 'I'})-[r]->(n) RETURN type(r), n.name, n.age"+ params = M.empty++-- | Test cypher basic query get relationships+case_cypherBasicQueryGetRels :: Assertion+case_cypherBasicQueryGetRels = withConnection host port $ do+ -- create initial data+ gp <- B.runBatch $ do+ n1 <- B.createNode $ M.fromList ["name" |: ("I" :: T.Text)]+ n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)]+ n3 <- B.createNode $ M.fromList ["name" |: ("him" :: T.Text), "age" |: (25 :: Int64)]+ _ <- B.createRelationship "know" M.empty n1 n2+ B.createRelationship "know" M.empty n1 n3+ -- actual test+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ let gres = G.addCypher (C.fromSuccess res) G.empty+ neo4jBool $ (length $ G.getRelationships gres) >= 2+ neo4jBool $ all (\r -> getRelType r == "know") (G.getRelationships gres)+ neo4jEqual ["r"] (C.cols $ C.fromSuccess res)+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp)+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "MATCH (x {name: 'I'})-[r]->(n) RETURN r"+ params = M.empty++-- | Test cypher nested results+case_cypherNestedResults :: Assertion+case_cypherNestedResults = withConnection host port $ do+ -- create initial data+ gp <- B.runBatch $ do+ n1 <- B.createNode $ M.fromList ["name" |: ("I" :: T.Text)]+ n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)]+ B.createRelationship "know" M.empty n1 n2+ -- actual test+ res <- C.cypher query params+ neo4jBool $ C.isSuccess res+ neo4jEqual ["collect(n.name)"] (C.cols $ C.fromSuccess res)+ let v = case (head $ head $ C.vals $ C.fromSuccess res) of+ J.Array vec -> vec+ _ -> V.empty+ neo4jBool $ "you" `V.elem` v+ neo4jBool $ "I" `V.elem` v+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp)+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "MATCH (n) WHERE n.name in ['I', 'you'] RETURN collect(n.name)"+ params = M.empty++-- | Test cypher errors+case_cypherError :: Assertion+case_cypherError = withConnection host port $ do+ -- create initial data+ gp <- B.runBatch $ do+ B.createNode $ M.fromList ["age" |: (26 :: Int64)]+ -- actual test+ res <- C.cypher query params+ neo4jEqual (Left "BadInputException") res+ -- cleanup+ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)+ return ()+ where query = "MATCH (x) WHERE has(x.age) RETURN x.age / 0"+ params = M.empty