haskell-neo4j-client 0.3.0.15 → 0.3.1.0
raw patch · 7 files changed
+458/−19 lines, 7 filesdep +network-uriPVP ok
version bump matches the API change (PVP)
Dependencies added: network-uri
API changes (from Hackage documentation)
+ Database.Neo4j.Batch: createNamedNode :: String -> Properties -> Batch (BatchFuture Node)
+ Database.Neo4j.Batch: createNamedRelationship :: (NodeBatchIdentifier a, NodeBatchIdentifier b) => String -> RelationshipType -> Properties -> a -> b -> Batch (BatchFuture Relationship)
+ Database.Neo4j.Batch: getNamedNode :: NodeBatchIdentifier a => String -> a -> Batch (BatchFuture Node)
+ Database.Neo4j.Batch: getNamedRelationship :: RelBatchIdentifier r => String -> r -> Batch (BatchFuture Relationship)
+ Database.Neo4j.Graph: addNamedNode :: String -> Node -> Graph -> Graph
+ Database.Neo4j.Graph: addNamedRelationship :: String -> Relationship -> Graph -> Graph
+ Database.Neo4j.Graph: getNamedNode :: String -> Graph -> Maybe Node
+ Database.Neo4j.Graph: getNamedRelationship :: String -> Graph -> Maybe Relationship
+ Database.Neo4j.Types: instance Eq Direction
+ Database.Neo4j.Types: instance Ord Node
+ Database.Neo4j.Types: instance Ord NodePath
+ Database.Neo4j.Types: instance Ord RelPath
+ Database.Neo4j.Types: instance Ord Relationship
+ Database.Neo4j.Types: instance Show Direction
Files
- haskell-neo4j-client.cabal +5/−2
- src/Database/Neo4j/Batch.hs +3/−2
- src/Database/Neo4j/Batch/Node.hs +12/−0
- src/Database/Neo4j/Batch/Relationship.hs +15/−0
- src/Database/Neo4j/Graph.hs +78/−12
- src/Database/Neo4j/Types.hs +7/−3
- tests/IntegrationTests.hs +338/−0
haskell-neo4j-client.cabal view
@@ -1,5 +1,5 @@ name: haskell-neo4j-client-version: 0.3.0.15+version: 0.3.1.0 synopsis: A Haskell neo4j client description: Library to interact with Neo4j databases. @@ -12,8 +12,9 @@ . -Batch calls for CRUD operations. .+ -Traversal API .- All code has been tested with Neo4j versions 2.0.3, 2.1.5, 2.1.7 and 2.2.0+ All code has been tested with Neo4j versions 2.0.3, 2.1.5, 2.1.7, 2.2.0 and 2.2.1 . 2.2.0 added authentication to the API this driver uses, this is not supported yet by this driver but it will be soon. In the meantime you will have to disable it.@@ -61,6 +62,7 @@ , lifted-base == 0.2.* , hashable == 1.2.* , mtl >= 2.1 && < 2.3+ , network-uri == 2.6.* library hs-source-dirs: src@@ -89,3 +91,4 @@ , hashable == 1.2.* , transformers-base == 0.4.* , mtl >= 2.1 && < 2.3+ , network-uri == 2.6.*
src/Database/Neo4j/Batch.hs view
@@ -8,9 +8,10 @@ -- * General Batch, runBatch, BatchFuture(..), NodeBatchIdentifier, RelBatchIdentifier, BatchEntity, -- * Nodes- createNode, getNode, deleteNode,+ createNode, createNamedNode, getNode, getNamedNode, deleteNode, -- * Relationships- createRelationship, getRelationship, getRelationshipFrom, getRelationshipTo, deleteRelationship, getRelationships,+ createRelationship, createNamedRelationship, getRelationship, getNamedRelationship, getRelationshipFrom,+ getRelationshipTo, deleteRelationship, getRelationships, -- * Properties setProperties, setProperty, deleteProperties, deleteProperty, -- * Labels
src/Database/Neo4j/Batch/Node.hs view
@@ -35,11 +35,23 @@ where cmd = defCmd{cmdMethod = HT.methodPost, cmdPath = "/node", cmdBody = J.toJSON props, cmdParse = parser} parser n = G.addNode (tryParseBody n) +-- | Batch operation to create a node and assign it a name to easily retrieve it from the resulting graph of the batch+createNamedNode :: String -> Properties -> Batch (BatchFuture Node)+createNamedNode name props = nextState cmd+ where cmd = defCmd{cmdMethod = HT.methodPost, cmdPath = "/node", cmdBody = J.toJSON props, cmdParse = parser}+ parser n = G.addNamedNode name (tryParseBody n)+ -- | Batch operation to get a node from the DB getNode :: NodeBatchIdentifier a => a -> Batch (BatchFuture Node) getNode n = nextState cmd where cmd = defCmd{cmdMethod = HT.methodGet, cmdPath = getNodeBatchId n, cmdBody = "", cmdParse = parser} parser jn = G.addNode (tryParseBody jn)++-- | Batch operation to get a node from the DB and assign it a name+getNamedNode :: NodeBatchIdentifier a => String -> a -> Batch (BatchFuture Node)+getNamedNode name n = nextState cmd+ where cmd = defCmd{cmdMethod = HT.methodGet, cmdPath = getNodeBatchId n, cmdBody = "", cmdParse = parser}+ parser jn = G.addNamedNode name (tryParseBody jn) -- | Batch operation to delete a node deleteNode :: NodeBatchIdentifier a => a -> Batch (BatchFuture ())
src/Database/Neo4j/Batch/Relationship.hs view
@@ -40,11 +40,26 @@ body = J.object ["to" .= getNodeBatchId nodeto, "type" .= t, "data" .= J.toJSON props] parser r = G.addRelationship (tryParseBody r) +-- | Create a new relationship with a type and a set of properties and assign it an identifier+createNamedRelationship :: (NodeBatchIdentifier a, NodeBatchIdentifier b) => String -> RelationshipType -> Properties+ -> a -> b -> Batch (BatchFuture Relationship)+createNamedRelationship name t props nodefrom nodeto = nextState cmd+ where cmd = defCmd{cmdMethod = HT.methodPost, cmdPath = path, cmdBody = J.toJSON body, cmdParse = parser}+ path = getNodeBatchId nodefrom <> "/relationships"+ body = J.object ["to" .= getNodeBatchId nodeto, "type" .= t, "data" .= J.toJSON props]+ parser r = G.addNamedRelationship name (tryParseBody r)+ -- | Refresh a relationship entity with the contents in the DB getRelationship :: RelBatchIdentifier r => r -> Batch (BatchFuture Relationship) getRelationship rel = nextState cmd where cmd = defCmd{cmdMethod = HT.methodGet, cmdPath = getRelBatchId rel, cmdBody = "", cmdParse = parser} parser jr = G.addRelationship (tryParseBody jr)++-- | Refresh a relationship entity with the contents in the DB and assign it an identifier+getNamedRelationship :: RelBatchIdentifier r => String -> r -> Batch (BatchFuture Relationship)+getNamedRelationship name rel = nextState cmd+ where cmd = defCmd{cmdMethod = HT.methodGet, cmdPath = getRelBatchId rel, cmdBody = "", cmdParse = parser}+ parser jr = G.addNamedRelationship name (tryParseBody jr) -- | Get the "node from" from a relationship from the DB getRelationshipFrom :: Relationship -> Batch (BatchFuture Node)
src/Database/Neo4j/Graph.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} + -- | Module to handle 'Graph' objects. These have information about a group of nodes, relationships, -- and information about labels per node and nodes per label. -- Notice a graph can have relationships and at the same time not have some of the nodes of those@@ -9,10 +10,10 @@ -- * General objects Graph, LabelSet, empty, -- * Handling nodes in the graph object- addNode, hasNode, deleteNode, getNodes, getNode, getNodeFrom, getNodeTo,+ addNode, addNamedNode, hasNode, deleteNode, getNodes, getNode, getNamedNode, getNodeFrom, getNodeTo, -- * Handling properties in the graph object- getRelationships, hasRelationship, addRelationship, deleteRelationship, getRelationshipNodeFrom,- getRelationshipNodeTo, getRelationship,+ getRelationships, hasRelationship, addRelationship, addNamedRelationship, deleteRelationship,+ getRelationshipNodeFrom, getRelationshipNodeTo, getRelationship, getNamedRelationship, -- ** Handling orphaned relationships #orphan# getOrphansFrom, getOrphansTo, cleanOrphanRelationships, -- * Handling properties@@ -50,18 +51,27 @@ type NodeRelIndex = M.HashMap NodePath RelSet data Graph = Graph {nodes :: NodeIndex, labels :: LabelNodeIndex, rels :: RelIndex,- nodeLabels :: NodeLabelIndex, nodeFromRels :: NodeRelIndex, nodeToRels :: NodeRelIndex+ nodeLabels :: NodeLabelIndex, nodeFromRels :: NodeRelIndex, nodeToRels :: NodeRelIndex,+ namedNodes :: M.HashMap String NodePath, namedRels :: M.HashMap String RelPath,+ nodeNames :: M.HashMap NodePath String, relNames :: M.HashMap RelPath String } deriving (Eq, Show) -- | Create an empty graph empty :: Graph empty = Graph {nodes = M.empty, labels = M.empty, rels = M.empty, nodeLabels = M.empty, nodeFromRels = M.empty,- nodeToRels = M.empty}+ nodeToRels = M.empty, namedNodes = M.empty, namedRels = M.empty, nodeNames = M.empty,+ relNames = M.empty} -- | Add a node to the graph addNode :: Node -> Graph -> Graph addNode n g = g {nodes = M.insert (getNodePath n) n (nodes g)} +-- | Add a node to the graph with an identifier+addNamedNode :: String -> Node -> Graph -> Graph+addNamedNode name n g = addNode n $ g {nodeNames = M.insert _nodePath name $ nodeNames g,+ namedNodes = M.insert name _nodePath $ namedNodes g}+ where _nodePath = getNodePath n+ -- | Set the properties of a node or relationship in the graph, if not present it won't do anything setProperties :: EntityIdentifier a => a -> Properties -> Graph -> Graph setProperties ei props g = fromMaybe g $ (addEntity . newEntity) <$> entity@@ -118,6 +128,12 @@ getNode :: NodeIdentifier a => a -> Graph -> Maybe Node getNode n g = getNodePath n `M.lookup` nodes g +-- | Get a node by name in the graph, if any+getNamedNode :: String -> Graph -> Maybe Node+getNamedNode name g = do+ nodepath <- getNodePath <$> name `M.lookup` namedNodes g+ nodepath `M.lookup` nodes g+ -- | Get outgoing relationships from a node getNodeFrom :: NodeIdentifier a => a -> Graph -> Maybe [Relationship] getNodeFrom n g = join $ sequence <$> filter (/=Nothing) <$> map getRel <$> fromrels@@ -142,11 +158,23 @@ deleteNode :: NodeIdentifier a => a -> Graph -> Graph deleteNode n g = g {nodes = M.delete nodeLoc (nodes g), labels = cleanLabelNodeIndex $ removeNodeFromLabels (labels g) labelsForNode,- nodeLabels = M.delete nodeLoc (nodeLabels g)}+ nodeLabels = M.delete nodeLoc (nodeLabels g), nodeNames = newNodeNames, namedNodes = newNamedNodes} where labelsForNode = M.lookupDefault HS.empty nodeLoc (nodeLabels g)+ nodepath = getNodePath n nodeLoc = getNodePath n removeNodeFromLabels = HS.foldl' (\acc x -> M.insertWith (\_ -> HS.delete nodeLoc) x HS.empty acc) cleanLabelNodeIndex = M.filter (/=HS.empty) + nodenames = nodeNames g+ (mNodeName, newNodeNames) = fromMaybe (Nothing, nodenames) $ do+ name <- nodepath `M.lookup` nodenames+ return (Just name, nodepath `M.delete` nodenames)+ namednodes = namedNodes g+ newNamedNodes = fromMaybe namednodes $ do+ nodename <- mNodeName+ _nodepath <- nodename `M.lookup` namednodes+ return $ if nodepath == _nodepath+ then nodename `M.delete` namednodes+ else namednodes -- | Add a relationship to the graph addRelationship :: Relationship -> Graph -> Graph@@ -157,6 +185,12 @@ pathTo = getNodePath . relTo relpath = getRelPath r +-- | Add a relationship to the graph with an identified+addNamedRelationship :: String -> Relationship -> Graph -> Graph+addNamedRelationship name r g = addRelationship r $ g {relNames = M.insert _relPath name $ relNames g,+ namedRels = M.insert name _relPath $ namedRels g}+ where _relPath = getRelPath r+ -- | Get a list with all the relationships in the graph getRelationships :: Graph -> [Relationship] getRelationships g = M.elems $ rels g@@ -165,6 +199,12 @@ getRelationship :: RelIdentifier a => a -> Graph -> Maybe Relationship getRelationship r g = getRelPath r `M.lookup` rels g +-- | Get a relationship by name in the graph, if any+getNamedRelationship :: String -> Graph -> Maybe Relationship+getNamedRelationship name g = do+ relpath <- getRelPath <$> name `M.lookup` namedRels g+ relpath `M.lookup` rels g+ -- | Get relationships missing their "from" node getOrphansFrom :: Graph -> [Relationship] getOrphansFrom g = M.elems $ M.filter noNode (rels g)@@ -183,14 +223,26 @@ deleteRelationship :: RelIdentifier a => a -> Graph -> Graph deleteRelationship r g = g {rels = M.delete (getRelPath r) (rels g), nodeFromRels = removeNodeRef (nodeFromRels g) pathFrom,- nodeToRels = removeNodeRef (nodeToRels g) pathTo}+ nodeToRels = removeNodeRef (nodeToRels g) pathTo,+ relNames = newRelNames, namedRels = newNamedRels} where updNodeRel = const $ HS.delete (getRelPath r)- rel = M.lookup (getRelPath r) (rels g)+ relpath = getRelPath r+ rel = M.lookup relpath (rels g) pathFrom = (getNodePath . relFrom) <$> rel pathTo = (getNodePath . relTo) <$> rel removeNodeRef nodeRel (Just nodepath) = M.insertWith updNodeRel nodepath HS.empty nodeRel removeNodeRef nodeRel Nothing = nodeRel-+ relnames = relNames g+ (mRelName, newRelNames) = fromMaybe (Nothing, relnames) $ do+ name <- relpath `M.lookup` relnames+ return (Just name, relpath `M.delete` relnames)+ namedrels = namedRels g+ newNamedRels = fromMaybe namedrels $ do+ relname <- mRelName+ _relpath <- relname `M.lookup` namedrels+ return $ if relpath == _relpath+ then relname `M.delete` namedrels+ else namedrels -- | Get the "node from" from a relationship getRelationshipNodeFrom :: Relationship -> Graph -> Maybe Node@@ -241,22 +293,36 @@ -- | Add two graphs resulting in a graph with all the nodes, labels and relationships of both -- | If a node/entity is present in both the second one will be chosen union :: Graph -> Graph -> Graph-union ga gb = addLabels (addRels (addNodes ga gb) gb) gb+union ga gb = mergeRelNames (mergeNodeNames (addLabels (addRels (addNodes ga gb) gb) gb) gb) gb where addRels g1 g2 = foldl (\gacc r -> addRelationship r gacc) g1 (getRelationships g2) addNodes g1 g2 = foldl (flip addNode) g1 (getNodes g2) addLabels g1 g2 = foldl (\gacc n -> setNodeLabels n (HS.toList $ getNodeLabels n g2) gacc) g1 (getNodes g2)+ mergeNodeNames g1 g2 = g1 {nodeNames = nodeNames g2 `M.union` nodeNames g1,+ namedNodes = namedNodes g2 `M.union` namedNodes g1}+ mergeRelNames g1 g2 = g1 {relNames = relNames g2 `M.union` relNames g1,+ namedRels = namedRels g2 `M.union` namedRels g1} -- | Remove the nodes and relationships in the first graph that appear in the second difference :: Graph -> Graph -> Graph-difference ga gb = relationshipFilter relFilterFunc (nodeFilter nodeFilterFunc ga)+difference ga gb = relationshipFilter relFilterFunc (nodeFilter nodeFilterFunc ga) {+ nodeNames = newNodeNames, namedNodes = newNamedNodes, relNames = newRelNames, namedRels = newNamedRels} where relFilterFunc r = not $ hasRelationship r gb nodeFilterFunc n = not $ hasNode n gb+ newNodeNames = nodeNames ga `M.difference` nodeNames gb+ newNamedNodes = namedNodes ga `M.difference` namedNodes gb+ newRelNames = relNames ga `M.difference` relNames gb+ newNamedRels = namedRels ga `M.difference` namedRels gb -- | Have a graph that only has nodes and relationships that are present in both intersection :: Graph -> Graph -> Graph-intersection ga gb = relationshipFilter relFilterFunc (nodeFilter nodeFilterFunc ga)+intersection ga gb = relationshipFilter relFilterFunc (nodeFilter nodeFilterFunc ga) {+ nodeNames = newNodeNames, namedNodes = newNamedNodes, relNames = newRelNames, namedRels = newNamedRels} where relFilterFunc r = hasRelationship r gb nodeFilterFunc n = hasNode n gb+ newNodeNames = nodeNames ga `M.intersection` nodeNames gb+ newNamedNodes = namedNodes ga `M.intersection` namedNodes gb+ newRelNames = relNames ga `M.intersection` relNames gb+ newNamedRels = namedRels ga `M.intersection` namedRels gb -- | Feed a cypher result (from the old API) into a graph (looks for nodes and relationships and inserts them) addCypher :: C.Response -> Graph -> Graph
src/Database/Neo4j/Types.hs view
@@ -137,6 +137,8 @@ -- | Representation of a Neo4j node, has a location URI and a set of properties data Node = Node {nodePath :: NodePath, nodeProperties :: Properties} deriving (Show, Eq)+instance Ord Node where+ a <= b = nodePath a <= nodePath b -- | Get the properties of a node getNodeProperties :: Node -> Properties@@ -160,7 +162,7 @@ nodeAPITxt :: T.Text nodeAPITxt = "/db/data/node" -newtype NodePath = NodePath {runNodePath :: T.Text} deriving (Show, Eq, Generic)+newtype NodePath = NodePath {runNodePath :: T.Text} deriving (Show, Ord, Eq, Generic) instance Hashable NodePath class NodeIdentifier a where@@ -188,7 +190,7 @@ type RelationshipType = T.Text -- | Relationship direction-data Direction = Outgoing | Incoming | Any+data Direction = Outgoing | Incoming | Any deriving (Eq, Show) -- | Type for a relationship location newtype RelUrl = RelUrl {runRelUrl :: T.Text} deriving (Show, Eq, Generic)@@ -200,6 +202,8 @@ relProperties :: Properties, relFrom :: NodePath, relTo :: NodePath} deriving (Show, Eq)+instance Ord Relationship where+ a <= b = relPath a <= relPath b -- | Get the properties of a relationship getRelProperties :: Relationship -> Properties@@ -240,7 +244,7 @@ relationshipAPITxt :: T.Text relationshipAPITxt = "/db/data/relationship" -newtype RelPath = RelPath {runRelPath :: T.Text} deriving (Show, Eq, Generic)+newtype RelPath = RelPath {runRelPath :: T.Text} deriving (Show, Eq, Ord, Generic) instance Hashable RelPath class RelIdentifier a where
tests/IntegrationTests.hs view
@@ -2,11 +2,14 @@ {-# LANGUAGE FlexibleInstances, TemplateHaskell #-} module Main where +import Control.Applicative ((<$>)) import Control.Monad (void, join) import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Default import Data.Monoid (Monoid, mappend) import Data.Int (Int64) import Data.Maybe (fromJust)+import Data.String (fromString) import qualified Data.Aeson as J import qualified Data.ByteString as S@@ -15,6 +18,8 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Vector as V+import qualified Data.List as L+import qualified Network.HTTP.Types as HT import Control.Exception (handle) import Test.Framework.TH (defaultMainGenerator)@@ -26,6 +31,7 @@ import qualified Database.Neo4j.Cypher as C import qualified Database.Neo4j.Graph as G import qualified Database.Neo4j.Transactional.Cypher as TC+import qualified Database.Neo4j.Traversal as T (<>) :: Monoid a => a -> a -> a (<>) = mappend@@ -1590,3 +1596,335 @@ neo4jEqual 1 (length $ G.getNodes g4) neo4jEqual (head $ G.getRelationships g) (head $ G.getRelationships g4) neo4jEqual (head $ G.getNodes g) (head $ G.getNodes g4)++-- | Environment for traversal tests+setUpTraversalTest :: Neo4j G.Graph+setUpTraversalTest = B.runBatch $ do+ root <- _createNode "Root"+ johan <- _createNode "Johan"+ mattias <- _createNode "Mattias"+ emil <- _createNode "Emil"+ peter <- _createNode "Peter"+ tobias <- _createNode "Tobias"+ sara <- _createNode "Sara"+ gumersindo <- _createNode "Gumersindo"+ _createRel "Root-Johan" "knows" root johan+ _createRel "Root-Mattias" "knows" root mattias + _createRel "Johan-Emil" "knows" johan emil + _createRel "Emil-Peter" "knows" emil peter+ _createRel "Emil-Tobias" "knows" emil tobias+ _createRel "Tobias-Sara" "loves" tobias sara+ _createRel "Gumersindo-Tobias" "hates" gumersindo tobias+ _createRel "Gumersindo-Sara" "admires" sara gumersindo+ where _createNode name = B.createNamedNode name $ M.fromList ["name" |: (fromString name :: T.Text)]+ _createRel name t from to = void $ B.createNamedRelationship name t M.empty from to++-- | clean up a traversal test+cleanUpTraversalTest :: G.Graph -> Neo4j ()+cleanUpTraversalTest g = void $ B.runBatch $ do+ mapM_ B.deleteRelationship (G.getRelationships g)+ mapM_ B.deleteNode (G.getNodes g)+++-- | Test a traversal with an unexisting start+case_traversalNotFound :: Assertion+case_traversalNotFound = do+ g <- withConnection host port $ setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let expException = Neo4jNoEntityException $ (TE.encodeUtf8 . runNodePath . nodePath) start+ assertException expException $ withConnection host port $ do+ let rels = map (\n -> fromJust $ G.getNamedRelationship n g) ["Root-Johan", "Root-Mattias"]+ mapM_ deleteRelationship rels+ deleteNode start+ void $ T.traverseGetNodes def start+ withConnection host port $ cleanUpTraversalTest g++-- | Test a default traversal returning nodes+case_defaultTraversalNodes :: Assertion+case_defaultTraversalNodes = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ ns <- T.traverseGetNodes def start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Root", "Mattias", "Johan"]+ neo4jEqual (L.sort expected) (L.sort ns)+ cleanUpTraversalTest g++-- | Test a default traversal returning relationships+case_defaultTraversalRels :: Assertion+case_defaultTraversalRels = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ rs <- T.traverseGetRels def start+ let expected = map (\n -> fromJust $ G.getNamedRelationship n g) ["Root-Mattias", "Root-Johan"]+ neo4jEqual (L.sort expected) (L.sort rs)+ cleanUpTraversalTest g++-- | Test a default traversal returning id paths+case_defaultTraversalIdPath :: Assertion+case_defaultTraversalIdPath = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ ps <- L.sort <$> T.traverseGetPath def start+ neo4jEqual 3 (length ps)+ -- check we've received the expected paths+ let _getNodePath = \name -> nodePath $ fromJust $ G.getNamedNode name g+ let _getRelPath = \name -> relPath $ fromJust $ G.getNamedRelationship name g+ let checkPath = \p nodes rels -> do+ neo4jEqual (map _getNodePath nodes) (T.pathNodes p)+ let resRels = T.pathRels p+ neo4jEqual (map _getRelPath (map fst rels)) (map fst resRels)+ neo4jEqual (map snd rels) (map snd resRels)+ checkPath (ps !! 0) ["Root"] []+ checkPath (ps !! 1) ["Root", "Johan"] [("Root-Johan", T.Out)]+ checkPath (ps !! 2) ["Root", "Mattias"] [("Root-Mattias", T.Out)]+ cleanUpTraversalTest g++-- | Test a default traversal returning full paths+case_defaultTraversalFullPath :: Assertion+case_defaultTraversalFullPath = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ ps <- L.sort <$> T.traverseGetFullPath def start+ neo4jEqual 3 (length ps)+ -- check we've received the expected paths+ let _getNode = \name -> fromJust $ G.getNamedNode name g+ let _getRel = \name -> fromJust $ G.getNamedRelationship name g+ let checkPath = \p nodes rels -> do+ neo4jEqual (L.sort $ map _getNode nodes) (L.sort $ T.pathNodes p)+ neo4jEqual (L.sort $ map _getRel rels) (L.sort $ T.pathRels p)+ checkPath (ps !! 0) ["Root"] []+ checkPath (ps !! 1) ["Root", "Johan"] ["Root-Johan"]+ checkPath (ps !! 2) ["Root", "Mattias"] ["Root-Mattias"]+ cleanUpTraversalTest g++-- | Test a traversal with an unexisting start+case_pagedTraversalNotFound :: Assertion+case_pagedTraversalNotFound = do+ g <- withConnection host port $ setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let expException = Neo4jNoEntityException $ (TE.encodeUtf8 . runNodePath . nodePath) start+ assertException expException $ withConnection host port $ do+ let rels = map (\n -> fromJust $ G.getNamedRelationship n g) ["Root-Johan", "Root-Mattias"]+ mapM_ deleteRelationship rels+ deleteNode start+ void $ T.pagedTraverseGetNodes def def start+ withConnection host port $ cleanUpTraversalTest g++-- | Test a paged traversal returning nodes+case_pagedTraversalNodes :: Assertion+case_pagedTraversalNodes = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travDepth = Left 2}+ let paging = def {T.pageSize = 1}+ pg <- T.pagedTraverseGetNodes desc paging start+ let _getNode = \name -> fromJust $ G.getNamedNode name g+ neo4jEqual False (T.pagedTraversalDone pg)+ neo4jEqual [_getNode "Root"] (T.getPagedValues pg)+ pg2 <- T.nextTraversalPage pg+ neo4jEqual False (T.pagedTraversalDone pg2)+ pg3 <- T.nextTraversalPage pg2+ neo4jEqual False (T.pagedTraversalDone pg3)+ neo4jEqual (L.sort $ map _getNode ["Johan", "Mattias"]) (L.sort $ T.getPagedValues pg2 ++ T.getPagedValues pg3)+ pg4 <- T.nextTraversalPage pg3+ neo4jEqual False (T.pagedTraversalDone pg4)+ neo4jEqual [_getNode "Emil"] (T.getPagedValues pg4)+ pg5 <- T.nextTraversalPage pg4+ neo4jEqual True (T.pagedTraversalDone pg5)+ pg6 <- T.nextTraversalPage pg5+ neo4jEqual True (T.pagedTraversalDone pg6)+ cleanUpTraversalTest g++-- | Test a paged traversal returning relationships+case_pagedTraversalRels :: Assertion+case_pagedTraversalRels = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def+ let paging = def {T.pageSize = 1}+ pg <- T.pagedTraverseGetRels desc paging start+ let _getRel = \name -> fromJust $ G.getNamedRelationship name g+ neo4jEqual False (T.pagedTraversalDone pg)+ pg2 <- T.nextTraversalPage pg+ neo4jEqual False (T.pagedTraversalDone pg2)+ pg3 <- T.nextTraversalPage pg2+ neo4jEqual False (T.pagedTraversalDone pg3)+ neo4jEqual (L.sort $ map _getRel ["Root-Johan", "Root-Mattias"]) (+ L.sort $ T.getPagedValues pg ++ T.getPagedValues pg2 ++ T.getPagedValues pg3)+ pg4 <- T.nextTraversalPage pg3+ neo4jEqual True (T.pagedTraversalDone pg4)+ cleanUpTraversalTest g++-- | Test a paged traversal returning id paths +case_pagedTraversalIdPaths :: Assertion+case_pagedTraversalIdPaths = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def+ let paging = def {T.pageSize = 1}+ let _getNodePath = \name -> nodePath $ fromJust $ G.getNamedNode name g+ let _getRelPath = \name -> relPath $ fromJust $ G.getNamedRelationship name g+ let checkPath = \p nodes rels -> do+ neo4jEqual (map _getNodePath nodes) (T.pathNodes p)+ let resRels = T.pathRels p+ neo4jEqual (map _getRelPath (map fst rels)) (map fst resRels)+ neo4jEqual (map snd rels) (map snd resRels)+ pg <- T.pagedTraverseGetPath desc paging start+ neo4jEqual False (T.pagedTraversalDone pg)+ checkPath (T.getPagedValues pg !! 0) ["Root"] []+ pg2 <- T.nextTraversalPage pg+ neo4jEqual False (T.pagedTraversalDone pg2)+ pg3 <- T.nextTraversalPage pg2+ neo4jEqual False (T.pagedTraversalDone pg3)+ let ps = L.sort $ T.getPagedValues pg2 ++ T.getPagedValues pg3+ checkPath (ps !! 0) ["Root", "Johan"] [("Root-Johan", T.Out)]+ checkPath (ps !! 1) ["Root", "Mattias"] [("Root-Mattias", T.Out)]+ pg4 <- T.nextTraversalPage pg3+ neo4jEqual True (T.pagedTraversalDone pg4)+ cleanUpTraversalTest g++-- | Test a paged traversal returning full paths +case_pagedTraversalFullPaths :: Assertion+case_pagedTraversalFullPaths = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def+ let paging = def {T.pageSize = 1}+ let _getNode = \name -> fromJust $ G.getNamedNode name g+ let _getRel = \name -> fromJust $ G.getNamedRelationship name g+ let checkPath = \p nodes rels -> do+ neo4jEqual (L.sort $ map _getNode nodes) (L.sort $ T.pathNodes p)+ neo4jEqual (L.sort $ map _getRel rels) (L.sort $ T.pathRels p)+ pg <- T.pagedTraverseGetFullPath desc paging start+ neo4jEqual False (T.pagedTraversalDone pg)+ checkPath (T.getPagedValues pg !! 0) ["Root"] []+ pg2 <- T.nextTraversalPage pg+ neo4jEqual False (T.pagedTraversalDone pg2)+ pg3 <- T.nextTraversalPage pg2+ neo4jEqual False (T.pagedTraversalDone pg3)+ let ps = L.sort $ T.getPagedValues pg2 ++ T.getPagedValues pg3+ checkPath (ps !! 0) ["Root", "Johan"] ["Root-Johan"]+ checkPath (ps !! 1) ["Root", "Mattias"] ["Root-Mattias"]+ pg4 <- T.nextTraversalPage pg3+ neo4jEqual True (T.pagedTraversalDone pg4)+ cleanUpTraversalTest g++-- | Test traversal depth first search+case_dfsTraversalNodes :: Assertion+case_dfsTraversalNodes = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travDepth = Left 2, T.travOrder = T.DepthFirst}+ ns <- T.traverseGetNodes desc start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Root", "Johan", "Emil", "Mattias"]+ neo4jEqual (L.sort expected) (L.sort ns)+ cleanUpTraversalTest g++-- | Test traversal with a relation filter+case_relFilterTraversalNodes :: Assertion+case_relFilterTraversalNodes = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Tobias" g+ let desc = def {T.travRelFilter = [("loves", Outgoing)]}+ ns <- T.traverseGetNodes desc start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Sara"]+ neo4jEqual (L.sort expected) (L.sort ns)+ let desc2 = def {T.travRelFilter = [("hates", Incoming)]}+ ns2 <- T.traverseGetNodes desc2 start+ let expected2 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Gumersindo"]+ neo4jEqual (L.sort expected2) (L.sort ns2)+ let desc3 = def {T.travRelFilter = [("hates", Any)]}+ ns3 <- T.traverseGetNodes desc3 start+ let expected3 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Gumersindo"]+ neo4jEqual (L.sort expected3) (L.sort ns3)+ let desc4 = def {T.travRelFilter = [("hates", Incoming), ("loves", Outgoing)]}+ ns4 <- T.traverseGetNodes desc4 start+ let expected4 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Sara", "Gumersindo"]+ neo4jEqual (L.sort expected4) (L.sort ns4)+ let desc5 = def {T.travRelFilter = [("htes", Incoming)]}+ ns5 <- T.traverseGetNodes desc5 start+ let expected5 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias"]+ neo4jEqual (L.sort expected5) (L.sort ns5)+ cleanUpTraversalTest g++-- | Test traversal with a uniqueness constraint+case_uniquenessTraversalNodes :: Assertion+case_uniquenessTraversalNodes = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Tobias" g+ let desc = def {T.travDepth = Left 2, T.travRelFilter = [("loves", Any), ("hates", Any), ("admires", Any)]}+ ns <- T.traverseGetNodes desc start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Sara", "Gumersindo", "Sara", "Gumersindo"]+ neo4jEqual (L.sort expected) (L.sort ns)+ let desc2 = desc {T.travUniqueness = Just T.NodeGlobal}+ ns2 <- T.traverseGetNodes desc2 start+ let expected2 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Sara", "Gumersindo"]+ neo4jEqual (L.sort expected2) (L.sort ns2)+ let desc3 = desc {T.travUniqueness = Just T.RelationshipGlobal}+ ns3 <- T.traverseGetNodes desc3 start+ let expected3 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Gumersindo", "Sara", "Gumersindo"]+ neo4jEqual (L.sort expected3) (L.sort ns3)+ let desc4 = desc {T.travUniqueness = Just T.NodePathUnique}+ ns4 <- T.traverseGetNodes desc4 start+ let expected4 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Sara", "Gumersindo", "Sara", "Gumersindo"]+ neo4jEqual (L.sort expected4) (L.sort ns4)+ let desc5 = desc {T.travUniqueness = Just T.RelationshipPath}+ ns5 <- T.traverseGetNodes desc5 start+ let expected5 = map (\n -> fromJust $ G.getNamedNode n g) ["Tobias", "Sara", "Gumersindo", "Sara", "Gumersindo"]+ neo4jEqual (L.sort expected5) (L.sort ns5)+ cleanUpTraversalTest g++-- | Test traversal with a javascript depth expression+case_progDepthTraversalNodes :: Assertion+case_progDepthTraversalNodes = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travDepth = Right "position.length() >= 2;"}+ ns <- T.traverseGetNodes desc start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Root", "Johan", "Mattias", "Emil"]+ neo4jEqual (L.sort expected) (L.sort ns)+ cleanUpTraversalTest g++-- | Test traversal with a wrong javascript depth expression+case_wrongProgDepthTraversalNodes :: Assertion+case_wrongProgDepthTraversalNodes = do+ g <- withConnection host port $ setUpTraversalTest+ assertException expException $ withConnection host port $ do+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travDepth = Right "positionlength() >= 2;"}+ void $ T.traverseGetNodes desc start+ withConnection host port $ cleanUpTraversalTest g+ where expException = Neo4jUnexpectedResponseException HT.status400++-- | Test traversal without initial node+case_returnButFirstTraversal :: Assertion+case_returnButFirstTraversal = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travNodeFilter = Left T.ReturnAllButStartNode}+ ns <- T.traverseGetNodes desc start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Johan", "Mattias"]+ neo4jEqual (L.sort expected) (L.sort ns)+ cleanUpTraversalTest g++-- | Test filter nodes without initial node+case_filterNodesTraversal :: Assertion+case_filterNodesTraversal = withConnection host port $ do+ g <- setUpTraversalTest+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travNodeFilter = Right "position.endNode().getProperty('name').toLowerCase().contains('t');"}+ ns <- T.traverseGetNodes desc start+ let expected = map (\n -> fromJust $ G.getNamedNode n g) ["Root", "Mattias"]+ neo4jEqual (L.sort expected) (L.sort ns)+ cleanUpTraversalTest g++-- | Test traversal with a filter expression+case_wrongFilterNodesTraversal :: Assertion+case_wrongFilterNodesTraversal = do+ g <- withConnection host port $ setUpTraversalTest+ assertException expException $ withConnection host port $ do+ let start = fromJust $ G.getNamedNode "Root" g+ let desc = def {T.travNodeFilter = Right "posiiiitionlength() >= 2;"}+ void $ T.traverseGetNodes desc start+ withConnection host port $ cleanUpTraversalTest g+ where expException = Neo4jUnexpectedResponseException HT.status400