{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} module Database.Bolt.Extras.Query.Get ( NodeGetter (..) , GraphGetRequest , GraphGetResponse , RelGetter (..) , getGraph , nodeAsText , condIdAsText ) where import Control.Monad.IO.Class (MonadIO) import Data.Map.Strict (Map, keys, mapWithKey, toList, (!)) import qualified Data.Text as T (Text, concat, empty, intercalate, pack) import Database.Bolt (BoltActionT, Node (..), Record, RecordValue (..), Relationship (..), URelationship (..), exact, query) import Database.Bolt.Extras.Graph (Graph (..)) import Database.Bolt.Extras.Persisted (BoltId) import Database.Bolt.Extras.Query.Cypher (ToCypher (..)) import Database.Bolt.Extras.Query.Utils (NodeName) import Database.Bolt.Extras.Template.Types (Label, Property) import NeatInterpolation (text) import Text.Printf (printf) -- | Helper to find 'Node's. -- data NodeGetter = NodeGetter { boltIdN :: Maybe BoltId , labelsN :: Maybe [Label] , propsN :: Maybe [Property] } deriving (Show) -- | Helper to find 'URelationship's. -- data RelGetter = RelGetter { labelR :: Maybe Label , propsR :: Maybe [Property] } deriving (Show) -- | The combinations of 'Getter's to load graph from the database. -- type GraphGetRequest = Graph NodeName NodeGetter RelGetter -- | The graph of 'Node's and 'URelationship's which we got from the database using 'GraphGetRequest'. -- type GraphGetResponse = Graph NodeName Node URelationship -- | For the given 'GraphGetRequest' find all graphs, which match it. -- This function creates single cypher query and performs it. -- getGraph :: (MonadIO m) => [T.Text] -> GraphGetRequest -> BoltActionT m [GraphGetResponse] getGraph customConds requestGraph = do response <- query (formQuery customConds nodeVars edgesVars vertices rels) mapM (\i -> do nodes <- sequence $ mapOnlyKey (fmap (!! i) . flip exactValues response) vertices edges <- sequence $ mapOnlyKey (fmap (makeU . (!! i)) . flip exactValues response . namesToText) rels return (Graph nodes edges)) [0.. length response - 1] where vertices :: Map NodeName NodeGetter vertices = _vertices requestGraph rels :: Map (NodeName, NodeName) RelGetter rels = _relations requestGraph nodeVars :: [T.Text] nodeVars = keys vertices edgesVars :: [T.Text] edgesVars = map (\k -> T.concat [fst k, "0", snd k]) (keys rels) exactValues :: (MonadIO m, RecordValue a) => T.Text -> [Record] -> BoltActionT m [a] exactValues var = mapM (exact . (! var)) makeU :: Relationship -> URelationship makeU Relationship{..} = URelationship relIdentity relType relProps namesToText :: (NodeName, NodeName) -> T.Text namesToText (nameA, nameB) = T.concat [nameA, "0", nameB] mapOnlyKey :: (k -> b) -> Map k a -> Map k b mapOnlyKey f = mapWithKey (\k _ -> f k) -- | This function creates cypher query, which is used for getting graph from the database. -- formQuery :: [T.Text] -> [T.Text] -> [T.Text] -> Map NodeName NodeGetter -> Map (NodeName, NodeName) RelGetter -> T.Text formQuery customConds returnNodes returnEdges vertices rels = [text|MATCH $completeRequest $conditionsQ RETURN $completeResponse|] where nodes = nodeAsText <$> toList vertices conditionsId = intercalateAnd . filter (/= "\n") $ fmap condIdAsText (toList vertices) customConditions = intercalateAnd customConds conditions = intercalateAnd . filter (/= T.empty) $ [conditionsId, customConditions] conditionsQ = if conditions == T.empty then "" else T.concat ["WHERE ", conditions] edges = fmap (relationshipAsText vertices) (toList rels) completeRequest = T.intercalate "," $ nodes ++ edges completeResponse = T.intercalate "," $ returnNodes ++ returnEdges intercalateAnd :: [T.Text] -> T.Text intercalateAnd = T.intercalate " AND " condIdAsText :: (NodeName, NodeGetter) -> T.Text condIdAsText (name, sel) = [text|$boltIdNR|] where boltIdNR = maybeNull (T.pack . printf "ID(%s)=%d" name) (boltIdN sel) nodeAsText :: (NodeName, NodeGetter) -> T.Text nodeAsText (name, sel) = [text|($name $labels $propsQ)|] where labels = maybeNull toCypher (labelsN sel) propsQ = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsN sel) relationshipAsText :: Map NodeName NodeGetter -> ((NodeName, NodeName), RelGetter) -> T.Text relationshipAsText vertices ((begNodeName, endNodeName), uRel) = [text|($begNodeName $begNodeLabels $begNodeProps)-[$name $typeQ $propsQ]-($endNodeName $endNodeLabels $endNodeProps)|] where name = T.concat [begNodeName, "0", endNodeName] typeQ = maybeNull toCypher (labelR uRel) begNode = vertices ! begNodeName begNodeLabels = maybeNull toCypher $ labelsN begNode begNodeProps = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsN begNode) endNode = vertices ! endNodeName endNodeLabels = maybeNull toCypher $ labelsN endNode endNodeProps = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsN endNode) propsQ = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsR uRel) maybeNull :: (a -> T.Text) -> Maybe a -> T.Text maybeNull = maybe ""