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
192 ------------------------------------------------------------------------
193 data RenameNode = RenameNode { r_name :: Text }
196 instance FromJSON RenameNode
197 instance ToJSON RenameNode
198 instance ToSchema RenameNode
199 instance Arbitrary RenameNode where
200 arbitrary = elements [RenameNode "test"]
201 ------------------------------------------------------------------------
202 data PostNode = PostNode { pn_name :: Text
203 , pn_typename :: NodeType}
206 instance FromJSON PostNode
207 instance ToJSON PostNode
208 instance ToSchema PostNode
209 instance Arbitrary PostNode where
210 arbitrary = elements [PostNode "Node test" NodeCorpus]
212 ------------------------------------------------------------------------
213 type DocsApi = Summary "Docs : Move to trash"
214 :> ReqBody '[JSON] Documents
215 :> Delete '[JSON] [Int]
217 data Documents = Documents { documents :: [NodeId]}
220 instance FromJSON Documents
221 instance ToJSON Documents
222 instance ToSchema Documents
224 delDocs :: CorpusId -> Documents -> Cmd err [Int]
225 delDocs cId ds = nodesToTrash $ map (\n -> (cId, n, True)) $ documents ds
227 ------------------------------------------------------------------------
228 type FavApi = Summary " Favorites label"
229 :> ReqBody '[JSON] Favorites
231 :<|> Summary " Favorites unlabel"
232 :> ReqBody '[JSON] Favorites
233 :> Delete '[JSON] [Int]
235 data Favorites = Favorites { favorites :: [NodeId]}
238 instance FromJSON Favorites
239 instance ToJSON Favorites
240 instance ToSchema Favorites
242 putFav :: CorpusId -> Favorites -> Cmd err [Int]
243 putFav cId fs = nodesToFavorite $ map (\n -> (cId, n, True)) $ favorites fs
245 delFav :: CorpusId -> Favorites -> Cmd err [Int]
246 delFav cId fs = nodesToFavorite $ map (\n -> (cId, n, False)) $ favorites fs
248 favApi :: CorpusId -> GargServer FavApi
249 favApi cId = putFav cId :<|> delFav cId
251 ------------------------------------------------------------------------
252 type TableApi = Summary " Table API"
253 :> QueryParam "view" TabType
254 :> QueryParam "offset" Int
255 :> QueryParam "limit" Int
256 :> QueryParam "order" OrderBy
257 :> Get '[JSON] [FacetDoc]
259 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
260 type PairingApi = Summary " Pairing API"
261 :> QueryParam "view" TabType -- TODO change TabType -> DocType (CorpusId for pairing)
262 :> QueryParam "offset" Int
263 :> QueryParam "limit" Int
264 :> QueryParam "order" OrderBy
265 :> Get '[JSON] [FacetDoc]
267 ------------------------------------------------------------------------
268 type ChartApi = Summary " Chart API"
269 :> QueryParam "from" UTCTime
270 :> QueryParam "to" UTCTime
271 :> Get '[JSON] (ChartMetrics Histo)
273 type PieApi = Summary " Chart API"
274 :> QueryParam "from" UTCTime
275 :> QueryParam "to" UTCTime
276 :> QueryParamR "ngramsType" TabType
277 :> Get '[JSON] (ChartMetrics Histo)
279 type TreeApi = Summary " Tree API"
280 :> QueryParam "from" UTCTime
281 :> QueryParam "to" UTCTime
282 :> QueryParamR "ngramsType" TabType
283 :> QueryParamR "listType" ListType
284 :> Get '[JSON] (ChartMetrics [MyTree])
288 -- Depending on the Type of the Node, we could post
289 -- New documents for a corpus
290 -- New map list terms
291 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
293 -- To launch a query and update the corpus
294 -- :<|> "query" :> Capture "string" Text :> Get '[JSON] Text
296 ------------------------------------------------------------------------
299 instance HasNodeError ServantErr where
300 _NodeError = prism' mk (const Nothing) -- panic "HasNodeError ServantErr: not a prism")
302 e = "Gargantext NodeError: "
303 mk NoListFound = err404 { errBody = e <> "No list found" }
304 mk NoRootFound = err404 { errBody = e <> "No Root found" }
305 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
306 mk NoUserFound = err404 { errBody = e <> "No User found" }
308 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
309 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
310 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
311 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
312 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
313 mk ManyParents = err500 { errBody = e <> "Too many parents" }
314 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
316 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
317 instance HasTreeError ServantErr where
318 _TreeError = prism' mk (const Nothing) -- panic "HasTreeError ServantErr: not a prism")
321 mk NoRoot = err404 { errBody = e <> "Root node not found" }
322 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
323 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
325 type TreeAPI = Get '[JSON] (Tree NodeTree)
326 -- TODO-ACCESS: CanTree or CanGetNode
327 -- TODO-EVENTS: No events as this is a read only query.
328 treeAPI :: NodeId -> GargServer TreeAPI
331 ------------------------------------------------------------------------
332 -- | Check if the name is less than 255 char
333 rename :: NodeId -> RenameNode -> Cmd err [Int]
334 rename nId (RenameNode name') = U.update (U.Rename nId name')
336 getTable :: NodeId -> Maybe TabType
337 -> Maybe Offset -> Maybe Limit
338 -> Maybe OrderBy -> Cmd err [FacetDoc]
339 getTable cId ft o l order =
341 (Just Docs) -> runViewDocuments cId False o l order
342 (Just Trash) -> runViewDocuments cId True o l order
343 _ -> panic "not implemented"
345 getPairing :: ContactId -> Maybe TabType
346 -> Maybe Offset -> Maybe Limit
347 -> Maybe OrderBy -> Cmd err [FacetDoc]
348 getPairing cId ft o l order =
350 (Just Docs) -> runViewAuthorsDoc cId False o l order
351 (Just Trash) -> runViewAuthorsDoc cId True o l order
352 _ -> panic "not implemented"
354 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
355 postNode uId pId (PostNode nodeName nt) = mkNodeWithParent nt (Just pId) uId nodeName
357 putNode :: NodeId -> Cmd err Int
358 putNode = undefined -- TODO
360 query :: Monad m => Text -> m Text
365 -- TODO Is it possible to adapt the function according to iValue input ?
366 --upload :: MultipartData -> Handler Text
367 --upload multipartData = do
369 -- putStrLn "Inputs:"
370 -- forM_ (inputs multipartData) $ \input ->
371 -- putStrLn $ " " <> show (iName input)
372 -- <> " -> " <> show (iValue input)
374 -- forM_ (files multipartData) $ \file -> do
375 -- content <- readFile (fdFilePath file)
376 -- putStrLn $ "Content of " <> show (fdFileName file)
377 -- <> " at " <> fdFilePath file
379 -- pure (pack "Data loaded")
381 -------------------------------------------------------------------------------
383 type MetricsAPI = Summary "SepGen IncExc metrics"
384 :> QueryParam "list" ListId
385 :> QueryParamR "ngramsType" TabType
386 :> QueryParam "limit" Int
387 :> Get '[JSON] Metrics
389 getMetrics :: NodeId -> GargServer MetricsAPI
390 getMetrics cId maybeListId tabType maybeLimit = do
391 (ngs', scores) <- Metrics.getMetrics' cId maybeListId tabType maybeLimit
394 metrics = map (\(Scored t s1 s2) -> Metric t (log' 5 s1) (log' 2 s2) (listType t ngs')) scores
395 log' n x = 1 + (if x <= 0 then 0 else (log $ (10^(n::Int)) * x))
396 listType t m = maybe (panic errorMsg) fst $ Map.lookup t m
397 errorMsg = "API.Node.metrics: key absent"
399 pure $ Metrics metrics