2 Module : Gargantext.API.Node
3 Description : Server API
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 -- TODO-SECURITY: Critical
12 -- TODO-ACCESS: CanGetNode
13 -- TODO-EVENTS: No events as this is a read only query.
16 -------------------------------------------------------------------
17 -- TODO-ACCESS: access by admin only.
18 -- At first let's just have an isAdmin check.
19 -- Later: check userId CanDeleteNodes Nothing
20 -- TODO-EVENTS: DeletedNodes [NodeId]
21 -- {"tag": "DeletedNodes", "nodes": [Int*]}
25 {-# OPTIONS_GHC -fno-warn-orphans #-}
27 {-# LANGUAGE DataKinds #-}
28 {-# LANGUAGE DeriveGeneric #-}
29 {-# LANGUAGE FlexibleContexts #-}
30 {-# LANGUAGE FlexibleInstances #-}
31 {-# LANGUAGE NoImplicitPrelude #-}
32 {-# LANGUAGE OverloadedStrings #-}
33 {-# LANGUAGE RankNTypes #-}
34 {-# LANGUAGE ScopedTypeVariables #-}
35 {-# LANGUAGE TemplateHaskell #-}
36 {-# LANGUAGE TypeOperators #-}
38 module Gargantext.API.Node
41 import Control.Lens ((^.))
42 import Control.Monad ((>>))
43 import Control.Monad.IO.Class (liftIO)
44 import Data.Aeson (FromJSON, ToJSON)
47 import Data.Text (Text())
48 import Data.Time (UTCTime)
49 import GHC.Generics (Generic)
50 import Gargantext.API.Auth (withAccess, PathId(..))
51 import Gargantext.API.Metrics
52 import Gargantext.API.Ngrams (TabType(..), TableNgramsApi, apiNgramsTableCorpus, QueryParamR)
53 import Gargantext.API.Ngrams.NTree (MyTree)
54 import Gargantext.API.Search (SearchDocsAPI, searchDocs)
55 import Gargantext.API.Table
56 import Gargantext.API.Types
57 import Gargantext.Core.Types (NodeTableResult)
58 import Gargantext.Core.Types.Main (Tree, NodeTree, ListType)
59 import Gargantext.Database.Config (nodeTypeId)
60 import Gargantext.Database.Facet (FacetDoc, OrderBy(..))
61 import Gargantext.Database.Node.Children (getChildren)
62 import Gargantext.Database.Schema.Node ( getNodesWithParentId, getNodeWith, getNode, deleteNode, deleteNodes, mkNodeWithParent, JSONB, HasNodeError(..))
63 import Gargantext.Database.Schema.NodeNode (nodeNodesCategory)
64 import Gargantext.Database.Tree (treeDB)
65 import Gargantext.Database.Types.Node
66 import Gargantext.Database.Utils -- (Cmd, CmdM)
67 import Gargantext.Prelude
68 import Gargantext.Viz.Chart
69 import Gargantext.Viz.Phylo.API (PhyloAPI, phyloAPI)
71 import Test.QuickCheck (elements)
72 import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)
73 import qualified Gargantext.Database.Node.Update as U (update, Update(..))
76 import qualified Gargantext.Text.List.Learn as Learn
77 import qualified Data.Vector as Vec
81 type NodesAPI = Delete '[JSON] Int
84 -- Be careful: really delete nodes
85 -- Access by admin only
86 nodesAPI :: [NodeId] -> GargServer NodesAPI
87 nodesAPI ids = deleteNodes ids
89 ------------------------------------------------------------------------
90 -- | TODO-ACCESS: access by admin only.
91 -- At first let's just have an isAdmin check.
92 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
93 -- To manage the Users roots
96 -- TODO needs design discussion.
97 type Roots = Get '[JSON] [Node HyperdataAny]
98 :<|> Put '[JSON] Int -- TODO
100 -- | TODO: access by admin only
101 roots :: GargServer Roots
102 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
103 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
105 -------------------------------------------------------------------
106 -- | Node API Types management
107 -- TODO-ACCESS : access by users
108 -- No ownership check is needed if we strictly follow the capability model.
110 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
112 -- CanRenameNode (or part of CanEditNode?)
113 -- CanCreateChildren (PostNodeApi)
114 -- CanEditNode / CanPutNode TODO not implemented yet
116 -- CanPatch (TableNgramsApi)
120 type NodeAPI a = Get '[JSON] (Node a)
121 :<|> "rename" :> RenameApi
122 :<|> PostNodeApi -- TODO move to children POST
124 :<|> Delete '[JSON] Int
125 :<|> "children" :> ChildrenApi a
128 :<|> "table" :> TableApi
129 :<|> "ngrams" :> TableNgramsApi
130 -- :<|> "pairing" :> PairingApi
132 :<|> "category" :> CatApi
133 :<|> "search" :> SearchDocsAPI
136 :<|> "metrics" :> ScatterAPI
137 :<|> "chart" :> ChartApi
139 :<|> "tree" :> TreeApi
140 :<|> "phylo" :> PhyloAPI
141 -- :<|> "add" :> NodeAddAPI
143 -- TODO-ACCESS: check userId CanRenameNode nodeId
144 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
145 type RenameApi = Summary " Rename Node"
146 :> ReqBody '[JSON] RenameNode
149 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
150 :> ReqBody '[JSON] PostNode
151 :> Post '[JSON] [NodeId]
153 type ChildrenApi a = Summary " Summary children"
154 :> QueryParam "type" NodeType
155 :> QueryParam "offset" Int
156 :> QueryParam "limit" Int
157 -- :> Get '[JSON] [Node a]
158 :> Get '[JSON] (NodeTableResult a)
160 ------------------------------------------------------------------------
161 type NodeNodeAPI a = Get '[JSON] (Node a)
163 nodeNodeAPI :: forall proxy a. (JSONB a, ToJSON a) => proxy a -> UserId -> CorpusId -> NodeId -> GargServer (NodeNodeAPI a)
164 nodeNodeAPI p uId cId nId = withAccess (Proxy :: Proxy (NodeNodeAPI a)) Proxy uId (PathNodeNode cId nId) nodeNodeAPI'
166 nodeNodeAPI' :: GargServer (NodeNodeAPI a)
167 nodeNodeAPI' = getNodeWith nId p
171 ------------------------------------------------------------------------
172 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
173 nodeAPI :: forall proxy a. (JSONB a, ToJSON a) => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
174 nodeAPI p uId id = withAccess (Proxy :: Proxy (NodeAPI a)) Proxy uId (PathNode id) nodeAPI'
176 nodeAPI' :: GargServer (NodeAPI a)
177 nodeAPI' = getNodeWith id p
181 :<|> deleteNodeApi id
182 :<|> getChildren id p
186 :<|> apiNgramsTableCorpus id
187 -- :<|> getPairing id
188 -- :<|> getTableNgramsDoc id
199 -- :<|> nodeAddAPI id
200 -- :<|> postUpload id
202 deleteNodeApi id' = do
204 if _node_typename node == nodeTypeId NodeUser
205 then panic "not allowed" -- TODO add proper Right Management Type
208 ------------------------------------------------------------------------
209 data RenameNode = RenameNode { r_name :: Text }
212 -- TODO unPrefix "r_" FromJSON, ToJSON, ToSchema, adapt frontend.
213 instance FromJSON RenameNode
214 instance ToJSON RenameNode
215 instance ToSchema RenameNode
216 instance Arbitrary RenameNode where
217 arbitrary = elements [RenameNode "test"]
218 ------------------------------------------------------------------------
219 data PostNode = PostNode { pn_name :: Text
220 , pn_typename :: NodeType}
223 -- TODO unPrefix "pn_" FromJSON, ToJSON, ToSchema, adapt frontend.
224 instance FromJSON PostNode
225 instance ToJSON PostNode
226 instance ToSchema PostNode
227 instance Arbitrary PostNode where
228 arbitrary = elements [PostNode "Node test" NodeCorpus]
230 ------------------------------------------------------------------------
231 type CatApi = Summary " To Categorize NodeNodes: 0 for delete, 1/null neutral, 2 favorite"
232 :> ReqBody '[JSON] NodesToCategory
235 data NodesToCategory = NodesToCategory { ntc_nodesId :: [NodeId]
236 , ntc_category :: Int
240 -- TODO unPrefix "ntc_" FromJSON, ToJSON, ToSchema, adapt frontend.
241 instance FromJSON NodesToCategory
242 instance ToJSON NodesToCategory
243 instance ToSchema NodesToCategory
245 catApi :: CorpusId -> GargServer CatApi
248 putCat :: CorpusId -> NodesToCategory -> Cmd err [Int]
249 putCat cId cs' = nodeNodesCategory $ map (\n -> (cId, n, ntc_category cs')) (ntc_nodesId cs')
251 ------------------------------------------------------------------------
252 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
253 type PairingApi = Summary " Pairing API"
254 :> QueryParam "view" TabType
255 -- TODO change TabType -> DocType (CorpusId for pairing)
256 :> QueryParam "offset" Int
257 :> QueryParam "limit" Int
258 :> QueryParam "order" OrderBy
259 :> Get '[JSON] [FacetDoc]
261 ------------------------------------------------------------------------
262 type ChartApi = Summary " Chart API"
263 :> QueryParam "from" UTCTime
264 :> QueryParam "to" UTCTime
265 :> Get '[JSON] (ChartMetrics Histo)
267 type PieApi = Summary " Chart API"
268 :> QueryParam "from" UTCTime
269 :> QueryParam "to" UTCTime
270 :> QueryParamR "ngramsType" TabType
271 :> Get '[JSON] (ChartMetrics Histo)
273 type TreeApi = Summary " Tree API"
274 :> QueryParam "from" UTCTime
275 :> QueryParam "to" UTCTime
276 :> QueryParamR "ngramsType" TabType
277 :> QueryParamR "listType" ListType
278 :> Get '[JSON] (ChartMetrics [MyTree])
280 -- Depending on the Type of the Node, we could post
281 -- New documents for a corpus
282 -- New map list terms
283 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
285 ------------------------------------------------------------------------
288 NOTE: These instances are not necessary. However, these messages could be part
289 of a display function for NodeError/TreeError.
290 instance HasNodeError ServantErr where
291 _NodeError = prism' mk (const Nothing) -- panic "HasNodeError ServantErr: not a prism")
293 e = "Gargantext NodeError: "
294 mk NoListFound = err404 { errBody = e <> "No list found" }
295 mk NoRootFound = err404 { errBody = e <> "No Root found" }
296 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
297 mk NoUserFound = err404 { errBody = e <> "No User found" }
299 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
300 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
301 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
302 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
303 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
304 mk ManyParents = err500 { errBody = e <> "Too many parents" }
305 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
307 instance HasTreeError ServantErr where
308 _TreeError = prism' mk (const Nothing) -- panic "HasTreeError ServantErr: not a prism")
311 mk NoRoot = err404 { errBody = e <> "Root node not found" }
312 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
313 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
316 type TreeAPI = Get '[JSON] (Tree NodeTree)
318 treeAPI :: NodeId -> GargServer TreeAPI
321 ------------------------------------------------------------------------
322 -- | Check if the name is less than 255 char
323 rename :: NodeId -> RenameNode -> Cmd err [Int]
324 rename nId (RenameNode name') = U.update (U.Rename nId name')
326 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
327 postNode uId pId (PostNode nodeName nt) = do
328 nodeUser <- getNodeWith (NodeId uId) HyperdataUser
329 let uId' = nodeUser ^. node_userId
330 mkNodeWithParent nt (Just pId) uId' nodeName
332 putNode :: NodeId -> Cmd err Int
333 putNode = undefined -- TODO
335 -------------------------------------------------------------