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
11 -- TODO-ACCESS: CanGetNode
12 -- 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*]}
25 {-# OPTIONS_GHC -fno-warn-orphans #-}
27 {-# LANGUAGE DataKinds #-}
28 {-# LANGUAGE DeriveGeneric #-}
29 {-# LANGUAGE FlexibleContexts #-}
30 {-# LANGUAGE NoImplicitPrelude #-}
31 {-# LANGUAGE OverloadedStrings #-}
32 {-# LANGUAGE RankNTypes #-}
33 {-# LANGUAGE TemplateHaskell #-}
34 {-# LANGUAGE TypeOperators #-}
36 module Gargantext.API.Node
39 import Control.Lens (prism')
40 import Control.Monad ((>>))
41 import Control.Monad.IO.Class (liftIO)
42 import Data.Aeson (FromJSON, ToJSON)
44 import Data.Text (Text())
45 import Data.Time (UTCTime)
46 import GHC.Generics (Generic)
47 import Gargantext.API.Metrics
48 import Gargantext.API.Ngrams (TabType(..), TableNgramsApiPut, TableNgramsApiGet, tableNgramsPut, getTableNgramsCorpus, QueryParamR)
49 import Gargantext.API.Search ( SearchAPI, searchIn, SearchInQuery)
50 import Gargantext.API.Types
51 import Gargantext.Core.Types (Offset, Limit)
52 import Gargantext.Core.Types.Main (Tree, NodeTree, ListType)
53 import Gargantext.Database.Facet (FacetDoc , runViewDocuments, OrderBy(..),runViewAuthorsDoc)
54 import Gargantext.Database.Node.Children (getChildren)
55 import Gargantext.Database.Schema.Node ( getNodesWithParentId, getNode, deleteNode, deleteNodes, mkNodeWithParent, JSONB, NodeError(..), HasNodeError(..))
56 import Gargantext.Database.Schema.NodeNode (nodesToFavorite, nodesToTrash)
57 import Gargantext.Database.Tree (treeDB, HasTreeError(..), TreeError(..))
58 import Gargantext.Database.Types.Node
59 import Gargantext.Database.Utils -- (Cmd, CmdM)
60 import Gargantext.Prelude
61 import Gargantext.Text.Metrics (Scored(..))
62 import Gargantext.Viz.Phylo.API (PhyloAPI, phyloAPI)
63 import Gargantext.Viz.Chart
64 import Gargantext.API.Ngrams.NTree (MyTree)
66 import Test.QuickCheck (elements)
67 import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)
68 import qualified Data.Map as Map
69 import qualified Gargantext.Database.Metrics as Metrics
70 import qualified Gargantext.Database.Node.Update as U (update, Update(..))
73 import qualified Gargantext.Text.List.Learn as Learn
74 import qualified Data.Vector as Vec
78 type NodesAPI = Delete '[JSON] Int
81 -- Be careful: really delete nodes
82 -- Access by admin only
83 nodesAPI :: [NodeId] -> GargServer NodesAPI
84 nodesAPI ids = deleteNodes ids
86 ------------------------------------------------------------------------
87 -- | TODO-ACCESS: access by admin only.
88 -- At first let's just have an isAdmin check.
89 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
90 -- To manage the Users roots
93 -- TODO needs design discussion.
94 type Roots = Get '[JSON] [NodeAny]
95 :<|> Put '[JSON] Int -- TODO
97 -- | TODO: access by admin only
98 roots :: GargServer Roots
99 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
100 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
102 -------------------------------------------------------------------
103 -- | Node API Types management
104 -- TODO-ACCESS : access by users
105 -- No ownership check is needed if we strictly follow the capability model.
107 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
109 -- CanRenameNode (or part of CanEditNode?)
110 -- CanCreateChildren (PostNodeApi)
111 -- CanEditNode / CanPutNode TODO not implemented yet
113 -- CanPatch (TableNgramsApi)
116 type NodeAPI a = Get '[JSON] (Node a)
117 :<|> "rename" :> RenameApi
118 :<|> PostNodeApi -- TODO move to children POST
120 :<|> Delete '[JSON] Int
121 :<|> "children" :> ChildrenApi a
124 :<|> "table" :> TableApi
125 :<|> "listGet" :> TableNgramsApiGet
126 :<|> "list" :> TableNgramsApiPut
127 :<|> "pairing" :> PairingApi
129 :<|> "favorites" :> FavApi
130 :<|> "documents" :> DocsApi
131 :<|> "search":> Summary "Node Search"
132 :> ReqBody '[JSON] SearchInQuery
133 :> QueryParam "offset" Int
134 :> QueryParam "limit" Int
135 :> QueryParam "order" OrderBy
139 :<|> "metrics" :> MetricsAPI
140 :<|> "chart" :> ChartApi
142 :<|> "tree" :> TreeApi
143 :<|> "phylo" :> PhyloAPI
145 -- TODO-ACCESS: check userId CanRenameNode nodeId
146 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
147 type RenameApi = Summary " Rename Node"
148 :> ReqBody '[JSON] RenameNode
151 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
152 :> ReqBody '[JSON] PostNode
153 :> Post '[JSON] [NodeId]
155 type ChildrenApi a = Summary " Summary children"
156 :> QueryParam "type" NodeType
157 :> QueryParam "offset" Int
158 :> QueryParam "limit" Int
159 :> Get '[JSON] [Node a]
160 ------------------------------------------------------------------------
161 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
162 nodeAPI :: JSONB a => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
169 :<|> getChildren id p
173 :<|> getTableNgramsCorpus id
176 -- :<|> getTableNgramsDoc id
191 ------------------------------------------------------------------------
192 data RenameNode = RenameNode { r_name :: Text }
195 instance FromJSON RenameNode
196 instance ToJSON RenameNode
197 instance ToSchema RenameNode
198 instance Arbitrary RenameNode where
199 arbitrary = elements [RenameNode "test"]
200 ------------------------------------------------------------------------
201 data PostNode = PostNode { pn_name :: Text
202 , pn_typename :: NodeType}
205 instance FromJSON PostNode
206 instance ToJSON PostNode
207 instance ToSchema PostNode
208 instance Arbitrary PostNode where
209 arbitrary = elements [PostNode "Node test" NodeCorpus]
211 ------------------------------------------------------------------------
212 type DocsApi = Summary "Docs : Move to trash"
213 :> ReqBody '[JSON] Documents
214 :> Delete '[JSON] [Int]
216 data Documents = Documents { documents :: [NodeId]}
219 instance FromJSON Documents
220 instance ToJSON Documents
221 instance ToSchema Documents
223 delDocs :: CorpusId -> Documents -> Cmd err [Int]
224 delDocs cId ds = nodesToTrash $ map (\n -> (cId, n, True)) $ documents ds
226 ------------------------------------------------------------------------
227 type FavApi = Summary " Favorites label"
228 :> ReqBody '[JSON] Favorites
230 :<|> Summary " Favorites unlabel"
231 :> ReqBody '[JSON] Favorites
232 :> Delete '[JSON] [Int]
234 data Favorites = Favorites { favorites :: [NodeId]}
237 instance FromJSON Favorites
238 instance ToJSON Favorites
239 instance ToSchema Favorites
241 putFav :: CorpusId -> Favorites -> Cmd err [Int]
242 putFav cId fs = nodesToFavorite $ map (\n -> (cId, n, True)) $ favorites fs
244 delFav :: CorpusId -> Favorites -> Cmd err [Int]
245 delFav cId fs = nodesToFavorite $ map (\n -> (cId, n, False)) $ favorites fs
247 favApi :: CorpusId -> GargServer FavApi
248 favApi cId = putFav cId :<|> delFav cId
250 ------------------------------------------------------------------------
251 type TableApi = Summary " Table API"
252 :> QueryParam "view" TabType
253 :> QueryParam "offset" Int
254 :> QueryParam "limit" Int
255 :> QueryParam "order" OrderBy
256 :> Get '[JSON] [FacetDoc]
258 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
259 type PairingApi = Summary " Pairing API"
260 :> QueryParam "view" TabType -- TODO change TabType -> DocType (CorpusId for pairing)
261 :> QueryParam "offset" Int
262 :> QueryParam "limit" Int
263 :> QueryParam "order" OrderBy
264 :> Get '[JSON] [FacetDoc]
266 ------------------------------------------------------------------------
267 type ChartApi = Summary " Chart API"
268 :> QueryParam "from" UTCTime
269 :> QueryParam "to" UTCTime
270 :> Get '[JSON] (ChartMetrics Histo)
272 type PieApi = Summary " Chart API"
273 :> QueryParam "from" UTCTime
274 :> QueryParam "to" UTCTime
275 :> QueryParamR "ngramsType" TabType
276 :> Get '[JSON] (ChartMetrics Histo)
278 type TreeApi = Summary " Tree API"
279 :> QueryParam "from" UTCTime
280 :> QueryParam "to" UTCTime
281 :> QueryParamR "ngramsType" TabType
282 :> QueryParamR "listType" ListType
283 :> Get '[JSON] (ChartMetrics [MyTree])
287 -- Depending on the Type of the Node, we could post
288 -- New documents for a corpus
289 -- New map list terms
290 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
292 -- To launch a query and update the corpus
293 -- :<|> "query" :> Capture "string" Text :> Get '[JSON] Text
295 ------------------------------------------------------------------------
298 instance HasNodeError ServantErr where
299 _NodeError = prism' mk (const Nothing) -- panic "HasNodeError ServantErr: not a prism")
301 e = "Gargantext NodeError: "
302 mk NoListFound = err404 { errBody = e <> "No list found" }
303 mk NoRootFound = err404 { errBody = e <> "No Root found" }
304 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
305 mk NoUserFound = err404 { errBody = e <> "No User found" }
307 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
308 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
309 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
310 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
311 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
312 mk ManyParents = err500 { errBody = e <> "Too many parents" }
313 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
315 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
316 instance HasTreeError ServantErr where
317 _TreeError = prism' mk (const Nothing) -- panic "HasTreeError ServantErr: not a prism")
320 mk NoRoot = err404 { errBody = e <> "Root node not found" }
321 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
322 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
324 type TreeAPI = Get '[JSON] (Tree NodeTree)
325 -- TODO-ACCESS: CanTree or CanGetNode
326 -- TODO-EVENTS: No events as this is a read only query.
327 treeAPI :: NodeId -> GargServer TreeAPI
330 ------------------------------------------------------------------------
331 -- | Check if the name is less than 255 char
332 rename :: NodeId -> RenameNode -> Cmd err [Int]
333 rename nId (RenameNode name') = U.update (U.Rename nId name')
335 getTable :: NodeId -> Maybe TabType
336 -> Maybe Offset -> Maybe Limit
337 -> Maybe OrderBy -> Cmd err [FacetDoc]
338 getTable cId ft o l order =
340 (Just Docs) -> runViewDocuments cId False o l order
341 (Just Trash) -> runViewDocuments cId True o l order
342 _ -> panic "not implemented"
344 getPairing :: ContactId -> Maybe TabType
345 -> Maybe Offset -> Maybe Limit
346 -> Maybe OrderBy -> Cmd err [FacetDoc]
347 getPairing cId ft o l order =
349 (Just Docs) -> runViewAuthorsDoc cId False o l order
350 (Just Trash) -> runViewAuthorsDoc cId True o l order
351 _ -> panic "not implemented"
353 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
354 postNode uId pId (PostNode nodeName nt) = mkNodeWithParent nt (Just pId) uId nodeName
356 putNode :: NodeId -> Cmd err Int
357 putNode = undefined -- TODO
359 query :: Monad m => Text -> m Text
363 -------------------------------------------------------------
364 type MetricsAPI = Summary "SepGen IncExc metrics"
365 :> QueryParam "list" ListId
366 :> QueryParamR "ngramsType" TabType
367 :> QueryParam "limit" Int
368 :> Get '[JSON] Metrics
370 getMetrics :: NodeId -> GargServer MetricsAPI
371 getMetrics cId maybeListId tabType maybeLimit = do
372 (ngs', scores) <- Metrics.getMetrics' cId maybeListId tabType maybeLimit
375 metrics = map (\(Scored t s1 s2) -> Metric t (log' 5 s1) (log' 2 s2) (listType t ngs')) scores
376 log' n x = 1 + (if x <= 0 then 0 else (log $ (10^(n::Int)) * x))
377 listType t m = maybe (panic errorMsg) fst $ Map.lookup t m
378 errorMsg = "API.Node.metrics: key absent"
380 pure $ Metrics metrics