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.
15 -------------------------------------------------------------------
16 -- TODO-ACCESS: access by admin only.
17 -- At first let's just have an isAdmin check.
18 -- Later: check userId CanDeleteNodes Nothing
19 -- TODO-EVENTS: DeletedNodes [NodeId]
20 -- {"tag": "DeletedNodes", "nodes": [Int*]}
24 {-# OPTIONS_GHC -fno-warn-orphans #-}
26 {-# LANGUAGE DataKinds #-}
27 {-# LANGUAGE DeriveGeneric #-}
28 {-# LANGUAGE FlexibleContexts #-}
29 {-# LANGUAGE FlexibleInstances #-}
30 {-# LANGUAGE NoImplicitPrelude #-}
31 {-# LANGUAGE OverloadedStrings #-}
32 {-# LANGUAGE RankNTypes #-}
33 {-# LANGUAGE ScopedTypeVariables #-}
34 {-# LANGUAGE TemplateHaskell #-}
35 {-# LANGUAGE TypeOperators #-}
37 module Gargantext.API.Node
40 import Control.Lens ((^.))
41 import Data.Aeson (FromJSON, ToJSON)
44 import Data.Text (Text())
45 import Data.Time (UTCTime)
46 import GHC.Generics (Generic)
47 import Gargantext.API.Auth (withAccess, PathId(..))
48 import Gargantext.API.Metrics
49 import Gargantext.API.Ngrams (TabType(..), TableNgramsApi, apiNgramsTableCorpus, QueryParamR)
50 import Gargantext.API.Ngrams.NTree (MyTree)
51 import Gargantext.API.Search (SearchDocsAPI, searchDocs, SearchPairsAPI, searchPairs)
52 import Gargantext.API.Table
53 import Gargantext.API.Types
54 import Gargantext.Core.Types (NodeTableResult)
55 import Gargantext.Core.Types.Main (Tree, NodeTree, ListType)
56 import Gargantext.Database.Config (nodeTypeId)
57 import Gargantext.Database.Flow.Pairing (pairing)
58 import Gargantext.Database.Facet (FacetDoc, OrderBy(..))
59 import Gargantext.Database.Node.Children (getChildren)
60 import Gargantext.Database.Node.User (NodeUser)
61 import Gargantext.Database.Schema.Node (getNodesWithParentId, getNodeWith, getNode, deleteNode, deleteNodes, mkNodeWithParent, JSONB, HasNodeError(..), getNodeUser)
62 import Gargantext.Database.Schema.NodeNode -- (nodeNodesCategory, insertNodeNode, NodeNode(..))
63 import Gargantext.Database.Node.UpdateOpaleye (updateHyperdata)
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
80 type NodesAPI = Delete '[JSON] Int
83 -- Be careful: really delete nodes
84 -- Access by admin only
85 nodesAPI :: [NodeId] -> GargServer NodesAPI
86 nodesAPI ids = deleteNodes ids
88 ------------------------------------------------------------------------
89 -- | TODO-ACCESS: access by admin only.
90 -- At first let's just have an isAdmin check.
91 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
92 -- To manage the Users roots
95 -- TODO needs design discussion.
96 type Roots = Get '[JSON] [NodeUser]
97 :<|> Put '[JSON] Int -- TODO
99 -- | TODO: access by admin only
100 roots :: GargServer Roots
101 roots = getNodesWithParentId Nothing
102 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
104 -------------------------------------------------------------------
105 -- | Node API Types management
106 -- TODO-ACCESS : access by users
107 -- No ownership check is needed if we strictly follow the capability model.
109 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
111 -- CanRenameNode (or part of CanEditNode?)
112 -- CanCreateChildren (PostNodeApi)
113 -- CanEditNode / CanPutNode TODO not implemented yet
115 -- CanPatch (TableNgramsApi)
119 type NodeAPI a = Get '[JSON] (Node a)
120 :<|> "rename" :> RenameApi
121 :<|> PostNodeApi -- TODO move to children POST
122 :<|> ReqBody '[JSON] a :> Put '[JSON] Int
123 :<|> Delete '[JSON] Int
124 :<|> "children" :> ChildrenApi a
127 :<|> "table" :> TableApi
128 :<|> "ngrams" :> TableNgramsApi
130 :<|> "category" :> CatApi
131 :<|> "search" :> SearchDocsAPI
134 :<|> "pairwith" :> PairWith
135 :<|> "pairs" :> Pairs
136 :<|> "pairing" :> PairingApi
137 :<|> "searchPair" :> SearchPairsAPI
140 :<|> "metrics" :> ScatterAPI
141 :<|> "chart" :> ChartApi
143 :<|> "tree" :> TreeApi
144 :<|> "phylo" :> PhyloAPI
145 -- :<|> "add" :> NodeAddAPI
147 -- TODO-ACCESS: check userId CanRenameNode nodeId
148 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
149 type RenameApi = Summary " Rename Node"
150 :> ReqBody '[JSON] RenameNode
153 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
154 :> ReqBody '[JSON] PostNode
155 :> Post '[JSON] [NodeId]
157 type ChildrenApi a = Summary " Summary children"
158 :> QueryParam "type" NodeType
159 :> QueryParam "offset" Int
160 :> QueryParam "limit" Int
161 -- :> Get '[JSON] [Node a]
162 :> Get '[JSON] (NodeTableResult a)
164 ------------------------------------------------------------------------
165 type NodeNodeAPI a = Get '[JSON] (Node a)
167 nodeNodeAPI :: forall proxy a. (JSONB a, ToJSON a)
172 -> GargServer (NodeNodeAPI a)
173 nodeNodeAPI p uId cId nId = withAccess (Proxy :: Proxy (NodeNodeAPI a)) Proxy uId (PathNodeNode cId nId) nodeNodeAPI'
175 nodeNodeAPI' :: GargServer (NodeNodeAPI a)
176 nodeNodeAPI' = getNodeWith nId p
178 ------------------------------------------------------------------------
179 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
180 nodeAPI :: forall proxy a. (JSONB a, FromJSON a, ToJSON a) => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
181 nodeAPI p uId id = withAccess (Proxy :: Proxy (NodeAPI a)) Proxy uId (PathNode id) nodeAPI'
183 nodeAPI' :: GargServer (NodeAPI a)
184 nodeAPI' = getNodeWith id p
188 :<|> deleteNodeApi id
189 :<|> getChildren id p
193 :<|> apiNgramsTableCorpus id
209 -- :<|> nodeAddAPI id
210 -- :<|> postUpload id
212 deleteNodeApi id' = do
214 if _node_typename node == nodeTypeId NodeUser
215 then panic "not allowed" -- TODO add proper Right Management Type
218 ------------------------------------------------------------------------
219 data RenameNode = RenameNode { r_name :: Text }
222 -- TODO unPrefix "r_" FromJSON, ToJSON, ToSchema, adapt frontend.
223 instance FromJSON RenameNode
224 instance ToJSON RenameNode
225 instance ToSchema RenameNode
226 instance Arbitrary RenameNode where
227 arbitrary = elements [RenameNode "test"]
228 ------------------------------------------------------------------------
229 data PostNode = PostNode { pn_name :: Text
230 , pn_typename :: NodeType}
233 -- TODO unPrefix "pn_" FromJSON, ToJSON, ToSchema, adapt frontend.
234 instance FromJSON PostNode
235 instance ToJSON PostNode
236 instance ToSchema PostNode
237 instance Arbitrary PostNode where
238 arbitrary = elements [PostNode "Node test" NodeCorpus]
240 ------------------------------------------------------------------------
241 type CatApi = Summary " To Categorize NodeNodes: 0 for delete, 1/null neutral, 2 favorite"
242 :> ReqBody '[JSON] NodesToCategory
245 data NodesToCategory = NodesToCategory { ntc_nodesId :: [NodeId]
246 , ntc_category :: Int
250 -- TODO unPrefix "ntc_" FromJSON, ToJSON, ToSchema, adapt frontend.
251 instance FromJSON NodesToCategory
252 instance ToJSON NodesToCategory
253 instance ToSchema NodesToCategory
255 catApi :: CorpusId -> GargServer CatApi
258 putCat :: CorpusId -> NodesToCategory -> Cmd err [Int]
259 putCat cId cs' = nodeNodesCategory $ map (\n -> (cId, n, ntc_category cs')) (ntc_nodesId cs')
261 ------------------------------------------------------------------------
262 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
263 -- Pairing utilities to move elsewhere
264 type PairingApi = Summary " Pairing API"
265 :> QueryParam "view" TabType
266 -- TODO change TabType -> DocType (CorpusId for pairing)
267 :> QueryParam "offset" Int
268 :> QueryParam "limit" Int
269 :> QueryParam "order" OrderBy
270 :> Get '[JSON] [FacetDoc]
273 type Pairs = Summary "List of Pairs"
274 :> Get '[JSON] [AnnuaireId]
275 pairs :: CorpusId -> GargServer Pairs
277 ns <- getNodeNode cId
278 pure $ map _nn_node2_id ns
280 type PairWith = Summary "Pair a Corpus with an Annuaire"
281 :> "annuaire" :> Capture "annuaire_id" AnnuaireId
282 :> "list" :> Capture "list_id" ListId
285 pairWith :: CorpusId -> GargServer PairWith
286 pairWith cId aId lId = do
287 r <- pairing cId aId lId
288 _ <- insertNodeNode [ NodeNode cId aId Nothing Nothing]
291 ------------------------------------------------------------------------
292 type ChartApi = Summary " Chart API"
293 :> QueryParam "from" UTCTime
294 :> QueryParam "to" UTCTime
295 :> Get '[JSON] (ChartMetrics Histo)
297 type PieApi = Summary " Chart API"
298 :> QueryParam "from" UTCTime
299 :> QueryParam "to" UTCTime
300 :> QueryParamR "ngramsType" TabType
301 :> Get '[JSON] (ChartMetrics Histo)
303 type TreeApi = Summary " Tree API"
304 :> QueryParam "from" UTCTime
305 :> QueryParam "to" UTCTime
306 :> QueryParamR "ngramsType" TabType
307 :> QueryParamR "listType" ListType
308 :> Get '[JSON] (ChartMetrics [MyTree])
310 -- Depending on the Type of the Node, we could post
311 -- New documents for a corpus
312 -- New map list terms
313 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
315 ------------------------------------------------------------------------
318 NOTE: These instances are not necessary. However, these messages could be part
319 of a display function for NodeError/TreeError.
320 instance HasNodeError ServantErr where
321 _NodeError = prism' mk (const Nothing) -- panic "HasNodeError ServantErr: not a prism")
323 e = "Gargantext NodeError: "
324 mk NoListFound = err404 { errBody = e <> "No list found" }
325 mk NoRootFound = err404 { errBody = e <> "No Root found" }
326 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
327 mk NoUserFound = err404 { errBody = e <> "No User found" }
329 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
330 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
331 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
332 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
333 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
334 mk ManyParents = err500 { errBody = e <> "Too many parents" }
335 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
337 instance HasTreeError ServantErr where
338 _TreeError = prism' mk (const Nothing) -- panic "HasTreeError ServantErr: not a prism")
341 mk NoRoot = err404 { errBody = e <> "Root node not found" }
342 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
343 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
346 type TreeAPI = QueryParams "type" NodeType :> Get '[JSON] (Tree NodeTree)
348 treeAPI :: NodeId -> GargServer TreeAPI
351 ------------------------------------------------------------------------
352 -- | Check if the name is less than 255 char
353 rename :: NodeId -> RenameNode -> Cmd err [Int]
354 rename nId (RenameNode name') = U.update (U.Rename nId name')
356 postNode :: HasNodeError err
361 postNode uId pId (PostNode nodeName nt) = do
362 nodeUser <- getNodeUser (NodeId uId)
363 let uId' = nodeUser ^. node_userId
364 mkNodeWithParent nt (Just pId) uId' nodeName
366 putNode :: forall err a. (HasNodeError err, JSONB a, ToJSON a)
370 putNode n h = fromIntegral <$> updateHyperdata n h
371 -------------------------------------------------------------