{-| Module : Gargantext.Database.Tree Description : Tree of Resource Nodes built from Database Copyright : (c) CNRS, 2017-Present License : AGPL + CECILL v3 Maintainer : team@gargantext.org Stability : experimental Portability : POSIX Let a Root Node, return the Tree of the Node as a directed acyclic graph (Tree). -- TODO delete node, if not owned, then suppress the link only -- see Action/Delete.hs -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} module Gargantext.Database.Query.Tree ( module Gargantext.Database.Query.Tree.Error , isDescendantOf , isOwnedBy , isSharedWith , isIn , tree , tree_flat , TreeMode(..) , findNodesId , DbTreeNode(..) , dt_name , dt_nodeId , dt_typeId , dt_publish_policy , findShared , findNodes , findNodesWithType , NodeMode(..) , sharedTreeUpdate , dbTree , updateTree , recursiveParents , lookupPublishPolicy ) where import Control.Lens qualified as L import Control.Lens (view, toListOf, at, each, _Just, to, set, over) import Database.PostgreSQL.Simple ( Only(Only), In(In) ) import Database.PostgreSQL.Simple.SqlQQ ( sql ) import Data.List.NonEmpty qualified as NE import Data.List qualified as List import Data.List (tail, nub) import Data.Map.Strict (fromListWith, lookup) import Data.Text qualified as Text import Gargantext.Core ( fromDBid, HasDBid(toDBid) ) import Gargantext.Core.Types.Main (NodeTree(..), Tree(..)) import Gargantext.Database.Admin.Config () import Gargantext.Database.Admin.Types.Node import Gargantext.Database.Prelude import Gargantext.Database.Query.Table.Node.Error (HasNodeError) import Gargantext.Database.Query.Table.Node (getNode) import Gargantext.Database.Query.Table.Node (getUserRootPublicNode) import Gargantext.Database.Query.Table.NodeNode import Gargantext.Database.Query.Tree.Error import Gargantext.Database.Schema.Node import Gargantext.Prelude hiding (to) ------------------------------------------------------------------------ data DbTreeNode = DbTreeNode { _dt_nodeId :: NodeId , _dt_typeId :: Int , _dt_parentId :: Maybe NodeId , _dt_name :: Text , _dt_publish_policy :: Maybe NodePublishPolicy } deriving (Show) makeLenses ''DbTreeNode instance Eq DbTreeNode where (==) d1 d2 = (==) (_dt_nodeId d1) (_dt_nodeId d2) ------------------------------------------------------------------------ data TreeMode = TreeBasic | TreeAdvanced | TreeFirstLevel -- | Returns the Tree of Nodes in Database tree :: (HasTreeError err, HasNodeError err) => UserId -> TreeMode -> RootId -> [NodeType] -> DBQuery err x (Tree NodeTree) tree _ TreeBasic = tree_basic tree loggedInUserId TreeAdvanced = tree_advanced loggedInUserId tree loggedInUserId TreeFirstLevel = tree_first_level loggedInUserId -- | Tree basic returns the Tree of Nodes in Database -- (without shared folders) -- keeping this for teaching purpose only tree_basic :: (HasTreeError err, HasNodeError err) => RootId -> [NodeType] -> DBQuery err x (Tree NodeTree) tree_basic r nodeTypes = do trees <- toTree . toTreeParent . map db_tree_to_node_tree <$> dbTree r nodeTypes either treeError pure trees -- | Advanced mode of the Tree enables shared nodes tree_advanced :: (HasTreeError err, HasNodeError err) => UserId -> RootId -> [NodeType] -> DBQuery err x (Tree NodeTree) tree_advanced loggedInUserId r types = do trees <- tree_find_modes loggedInUserId [ Private, Public, Shared, Published ] r types either treeError pure (toTree . toSubtreeParent r $ trees) -- | Fetch only first level of tree tree_first_level :: (HasTreeError err, HasNodeError err) => UserId -> RootId -> [NodeType] -> DBQuery err x (Tree NodeTree) tree_first_level loggedInUserId r types = do trees <- tree_find_modes loggedInUserId [ Private, PublicDirect, SharedDirect, Published ] r types either treeError pure (toTree . toSubtreeParent r $ trees) -- | Fetch tree in a flattened form tree_flat :: (HasTreeError err, HasNodeError err) => UserId -> RootId -> [NodeType] -> Maybe Text -> DBQuery err x [NodeTree] tree_flat loggedInUserId r nodeTypes q = do trees <- map snd <$> tree_find_modes loggedInUserId [Private, Public, Shared] r nodeTypes pure $ case q of Just v -> filter (\(NodeTree {_nt_name}) -> Text.isInfixOf (Text.toLower v) (Text.toLower _nt_name)) trees Nothing -> trees -- | Collect all the subtrees given for the input 'NodeMode' list. tree_find_modes :: (HasTreeError err, HasNodeError err) => UserId -> [NodeMode] -> RootId -> [NodeType] -> DBQuery err x [(Maybe ParentId, NodeTree)] tree_find_modes loggedInUserId nodeModes r nodeTypes = do foldM (\ !acc mode -> findNodes loggedInUserId r mode nodeTypes <&> \dbTrees -> acc <> map db_tree_to_node_tree dbTrees) [] nodeModes db_tree_to_node_tree :: DbTreeNode -> (Maybe ParentId, NodeTree) db_tree_to_node_tree t = (_dt_parentId t, toNodeTree t) ------------------------------------------------------------------------ data NodeMode = -- | A node is /private/, i.e. only visible to the logged-in user. Private -- | A node is /shared/, i.e. only visible to the logged-in user /and/ -- the members of the team. | Shared -- | A node is /public/, i.e. visible to /all/ logged-in users. | Public -- | A node is /published/ when it's public and it's visible in all other -- user trees. The semantic between 'Public' and 'Published' is subtle: -- 'Public' refers to published nodes _of the current user_ which are visible -- in his/her \"Public\" folder. 'Published' refers to published nodes -- _from other users_, which will be listed in the final tree we build. | Published | SharedDirect | PublicDirect findNodes :: (HasTreeError err, HasNodeError err) => UserId -> RootId -> NodeMode -> [NodeType] -> DBQuery err x [DbTreeNode] findNodes _ r Private nt = dbTree r nt findNodes _ r Shared nt = findShared r NodeFolderShared nt sharedTreeUpdate findNodes loggedInUserId r SharedDirect nt = findSharedDirect loggedInUserId r NodeFolderShared nt sharedTreeUpdate findNodes _ r Public nt = findShared r NodeFolderPublic nt publicTreeUpdate findNodes loggedInUserId r PublicDirect nt = findSharedDirect loggedInUserId r NodeFolderPublic nt publicTreeUpdate findNodes loggedInUserId r Published nt = findPublished loggedInUserId r nt -- | Finds the /published/ nodes. Refer to the documentation for the 'NodeMode' to -- understand the difference between publish and published (which are the same thing, -- but seen from different perspectives). findPublished :: (HasTreeError err, HasNodeError err) => UserId -> RootId -> [NodeType] -> DBQuery err x [DbTreeNode] findPublished loggedInUserId _r nt = do -- Get the public folder for the /currently logged in user/. This will be the node where we -- want to attach all the /public nodes/ of the users that are publishing something. loggedInUserPublicFolder <- getUserRootPublicNode loggedInUserId -- Get all the published nodes, but filter out those owned by the currently logged in user, because -- those will be listed /only for the owner/ as part of the 'Public' 'find_nodes' query. allPublishedRootFolders <- filter (notOwnedByMe loggedInUserPublicFolder) <$> selectPublishedNodes trees <- forM allPublishedRootFolders $ \PublishedNodeInfo{pni_target_id, pni_owner_id, pni_policy} -> do let (TargetId publicFolderId) = pni_target_id let (OwnerId publicFolderOwnerId) = pni_owner_id publicNode <- getNode publicFolderId let publicNodeId = publicNode ^. node_id -- Fetch the user node which is sharing this folder. sharerUserNode <- getNode publicFolderOwnerId -- Get the full subtree reacheable from the source id sharedSubTree <- findNodes (sharerUserNode ^. node_user_id) publicFolderOwnerId Public nt -- Now we need to artificially attach this subtree to a node which will show up in -- the final tree as being owned by the logged-in user, but clearly showing as provenance -- the public folder id. let sharerDbTreeNode = DbTreeNode { _dt_nodeId = publicNodeId , _dt_typeId = publicNode ^. node_typename , _dt_parentId = Just (loggedInUserPublicFolder ^. node_id) , _dt_name = sharerUserNode ^. node_name -- It's the published node, not this artificial root that has to display the -- policy. , _dt_publish_policy = Nothing } let detachedTree = sharerDbTreeNode : (sharedSubTree & over L._head (\dt -> dt { _dt_parentId = Just publicNodeId , _dt_publish_policy = Just pni_policy })) pure $ detachedTree pure $ mconcat trees where notOwnedByMe :: Node a -> PublishedNodeInfo -> Bool notOwnedByMe myPublicFolder PublishedNodeInfo{pni_target_id} = myPublicFolder ^. node_id /= (_TargetId pni_target_id) ------------------------------------------------------------------------ -- | Collaborative Nodes in the Tree -- Queries the `nodes_nodes` table. findShared :: HasTreeError err => RootId -> NodeType -> [NodeType] -> UpdateTree err x -> DBQuery err x [DbTreeNode] findShared r nt nts fun = do foldersSharedId <- findNodesId r [nt] trees <- mapM (updateTree nts fun) foldersSharedId pure $ concat trees -- | Find shared folders with "direct" access, i.e. when fetching only -- first-level subcomponents. This works in a simplified manner: fetch the node -- and get the tree for its parent. findSharedDirect :: (HasTreeError err, HasNodeError err) => UserId -> RootId -> NodeType -> [NodeType] -> UpdateTree err x -> DBQuery err x [DbTreeNode] findSharedDirect _loggedInUserId r nt nts fun = do rootNode <- getNode r let mParent = _node_parent_id rootNode case mParent of Nothing -> pure [] Just parentId -> do foldersSharedId <- findNodesId parentId [nt] concat <$> mapM (updateTree nts fun) foldersSharedId -- | An action to update the nodes /AFTER/ they have been fetched from the database, -- so it's not an update in database terms, but it's actually a transformation on -- notes fetched from the DB as in a pure /Query/. type UpdateTree err x = ParentId -> [NodeType] -> NodeId -> DBQuery err x [DbTreeNode] updateTree :: HasTreeError err => [NodeType] -> UpdateTree err x -> RootId -> DBQuery err x [DbTreeNode] updateTree nts fun r = do folders <- getNodeNode r nodesSharedId <- mapM (fun r nts) $ map _nn_node2_id folders pure $ concat nodesSharedId sharedTreeUpdate :: HasTreeError err => UpdateTree err x sharedTreeUpdate p nt n = dbTree n nt <&> map (\n' -> if (view dt_nodeId n') == n -- && elem (fromDBid $ _dt_typeId n') [NodeGraph] -- && not (elem (fromDBid $ _dt_typeId n') [NodeFile]) then set dt_parentId (Just p) n' else n') publicTreeUpdate :: HasTreeError err => UpdateTree err x publicTreeUpdate p nt n = dbTree n nt <&> map (\n' -> if _dt_nodeId n' == n -- && (fromDBid $ _dt_typeId n') /= NodeGraph -- && not (elem (fromDBid $ _dt_typeId n') [NodeFile]) then set dt_parentId (Just p) n' else n') -- | findNodesId returns all nodes matching nodeType but the root (Nodeuser) findNodesId :: RootId -> [NodeType] -> DBQuery err x [NodeId] findNodesId r nt = tail <$> map _dt_nodeId <$> dbTree r nt findNodesWithType :: HasCallStack => RootId -> [NodeType] -> [NodeType] -> DBQuery err x [DbTreeNode] findNodesWithType root target through = filter isInTarget <$> dbTree root through where isInTarget n = List.elem (fromDBid $ view dt_typeId n) $ List.nub $ target <> through ------------------------------------------------------------------------ toTree :: Map (Maybe ParentId) [NodeTree] -> Either TreeError (Tree NodeTree) toTree m = case lookup Nothing m of Just [root] -> pure $ toTree' m root Nothing -> Left $ NoRoot Just [] -> Left $ EmptyRoot Just r -> Left $ TooManyRoots (NE.fromList $ map _nt_id r) where toTree' :: Map (Maybe ParentId) [NodeTree] -> NodeTree -> Tree NodeTree toTree' m' root = TreeN root $ -- Lines below are equivalent computationally but not semantically -- m' ^.. at (Just $ _dt_nodeId root) . _Just . each . to (toTree' m') toListOf (at (Just $ _nt_id root) . _Just . each . to (toTree' m')) m' -- FIXME(adn) We need to propagate the 'PublishPolicy' into a 'DbTreeNode' and a 'NodeTree' -- so that we can display them properly in the frontend. toNodeTree :: HasCallStack => DbTreeNode -> NodeTree toNodeTree (DbTreeNode nId tId _ n pp) = NodeTree n (fromDBid tId) nId pp ------------------------------------------------------------------------ toTreeParent :: [(Maybe ParentId, NodeTree)] -> Map (Maybe ParentId) [NodeTree] toTreeParent = fromListWith (\a b -> nub $ a <> b) . map (\(mb_parent, n) -> (mb_parent, [n])) ------------------------------------------------------------------------ -- toSubtreeParent' :: [DbTreeNode] -- -> Map (Maybe ParentId) [DbTreeNode] -- toSubtreeParent' ns = fromListWith (\a b -> nub $ a <> b) . map (\n -> (_dt_parentId n, [n])) $ nullifiedParents -- where -- nodeIds = Set.fromList $ map (\n -> unNodeId $ _dt_nodeId n) ns -- nullifiedParents = map nullifyParent ns -- nullifyParent dt@(DbTreeNode { _dt_parentId = Nothing }) = dt -- nullifyParent dt@(DbTreeNode { _dt_nodeId = nId -- , _dt_parentId = Just pId -- , _dt_typeId = tId -- , _dt_name = name }) = -- if Set.member (unNodeId pId) nodeIds then -- dt -- else -- DbTreeNode { _dt_nodeId = nId -- , _dt_typeId = tId -- , _dt_parentId = Nothing -- , _dt_name = name } ------------------------------------------------------------------------ toSubtreeParent :: RootId -> [(Maybe ParentId, NodeTree)] -> Map (Maybe ParentId) [NodeTree] toSubtreeParent r = toTreeParent . map (nullifyParent r) nullifyParent :: RootId -> (Maybe ParentId, NodeTree) -> (Maybe ParentId, NodeTree) nullifyParent _ (Nothing, t) = (Nothing, t) nullifyParent r (Just parent, t@(NodeTree{..})) = if r == _nt_id then (Nothing, t) else (Just parent, t) ------------------------------------------------------------------------ -- | Main DB Tree function dbTree :: RootId -> [NodeType] -> DBQuery err x [DbTreeNode] dbTree rootId nodeTypes = map (\(nId, tId, pId, n) -> DbTreeNode nId tId pId n Nothing) <$> mkPGQuery [sql| WITH RECURSIVE tree (id, typename, parent_id, name) AS ( SELECT p.id, p.typename, p.parent_id, p.name FROM nodes AS p WHERE p.id = ? UNION SELECT c.id, c.typename, c.parent_id, c.name FROM nodes AS c INNER JOIN tree AS s ON c.parent_id = s.id WHERE c.typename IN ? ) SELECT * from tree; |] (rootId, In typename) where typename = map toDBid ns ns = case nodeTypes of [] -> allNodeTypes _ -> nodeTypes isDescendantOf :: NodeId -> RootId -> DBQuery err x Bool isDescendantOf childId rootId = (== [Only True]) <$> mkPGQuery [sql| WITH RECURSIVE tree (id, parent_id) AS ( SELECT c.id, c.parent_id FROM nodes AS c WHERE c.id = ? UNION SELECT p.id, p.parent_id FROM nodes AS p INNER JOIN tree AS t ON t.parent_id = p.id ) SELECT COUNT(*) = 1 from tree AS t WHERE t.id = ?; |] (childId, rootId) isOwnedBy :: NodeId -> UserId -> DBQuery err x Bool isOwnedBy nodeId userId = (== [Only True]) <$> mkPGQuery [sql| SELECT COUNT(*) = 1 from nodes AS c where c.id = ? AND c.user_id = ? |] (nodeId, userId) isSharedWith :: NodeId -> NodeId -> DBQuery err x Bool isSharedWith targetNode targetUserNode = (== [Only True]) <$> mkPGQuery [sql| WITH RECURSIVE SharePath AS ( SELECT nn.node1_id, nn.node2_id AS shared_node_id FROM nodes_nodes nn WHERE nn.node1_id IN (SELECT id FROM nodes WHERE parent_id = ?) UNION ALL SELECT nn.node1_id, nn.node2_id FROM nodes_nodes nn JOIN SharePath sp ON nn.node1_id = sp.shared_node_id ), UpwardPath AS ( SELECT ? AS current_node_id, parent_id FROM nodes WHERE id = ? UNION ALL SELECT up.parent_id, n.parent_id FROM UpwardPath up JOIN nodes n ON up.parent_id = n.id ) SELECT EXISTS ( SELECT 1 FROM UpwardPath up JOIN SharePath sp ON up.current_node_id = sp.shared_node_id ) AS share_exists; |] (targetUserNode, targetNode, targetNode) -- TODO should we check the category? isIn :: NodeId -> DocId -> DBQuery err x Bool isIn cId docId = ( == [Only True]) <$> mkPGQuery [sql| SELECT COUNT(*) = 1 FROM nodes_nodes nn WHERE nn.node1_id = ? AND nn.node2_id = ?; |] (cId, docId) -- Recursive parents function to construct a breadcrumb recursiveParents :: NodeId -> [NodeType] -> DBQuery err x [DbTreeNode] recursiveParents nodeId nodeTypes = map (\(nId, tId, pId, n) -> DbTreeNode nId tId pId n Nothing) <$> mkPGQuery [sql| WITH RECURSIVE recursiveParents AS ( SELECT id, typename, parent_id, name, 1 as original_order FROM public.nodes WHERE id = ? UNION ALL SELECT n.id, n.typename, n.parent_id, n.name, rp.original_order+1 FROM public.nodes n INNER JOIN recursiveParents rp ON n.id = rp.parent_id WHERE n.typename IN ? ) SELECT id, typename, parent_id, name FROM recursiveParents ORDER BY original_order DESC; |] (nodeId, In typename) where typename = map toDBid ns ns = case nodeTypes of [] -> allNodeTypes _ -> nodeTypes ----------------------------------------------------- -- | Given an input 'NodeId', figures out if the node itself or -- a parent is published and, if yes, returns the original publish policy. -- NOTE(adn) This query is not very optimised, and once it starts to become slow, -- we need to rewrite it as a plain PG query to make it fast. lookupPublishPolicy :: HasNodeError err => NodeId -> DBQuery err x (Maybe NodePublishPolicy) lookupPublishPolicy targetId = do -- Optimisation: if the 'targetId' ends up being one of the published \"root\" node_node, -- short-circuit. mb_nn <- getNodeNode2 targetId case lookupPublish mb_nn of Just pol -> pure $ Just pol Nothing -> do isRO <- isNodeReadOnly targetId case isRO of False -> pure Nothing True -> do -- General case: find all the recursive parents for the target id, and for each of them -- check if they are read only; if yes, grab their 'NodePublishPolicy' allParents <- recursiveParents targetId [] go Nothing allParents where lookupPublish mb_nn = mb_nn >>= \nn -> nn L.^? (nn_category . _Just . _NNC_publish) go !acc [] = pure acc go !acc (x:xs) = do isRO <- isNodeReadOnly (_dt_nodeId x) case isRO of True -> do mb_nn <- getNodeNode2 (_dt_nodeId x) case lookupPublish mb_nn of Just pol -> pure $ Just pol Nothing -> go acc xs False -> go acc xs