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
13 {-# OPTIONS_GHC -fno-warn-orphans #-}
15 {-# LANGUAGE DataKinds #-}
16 {-# LANGUAGE DeriveGeneric #-}
17 {-# LANGUAGE FlexibleContexts #-}
18 {-# LANGUAGE NoImplicitPrelude #-}
19 {-# LANGUAGE OverloadedStrings #-}
20 {-# LANGUAGE RankNTypes #-}
21 {-# LANGUAGE TemplateHaskell #-}
22 {-# LANGUAGE TypeOperators #-}
24 module Gargantext.API.Node
25 ( module Gargantext.API.Node
27 , HyperdataAnnuaire(..)
29 , HyperdataResource(..)
31 , HyperdataDocument(..)
32 , HyperdataDocumentV3(..)
35 import Control.Lens (prism', set)
36 import Control.Monad ((>>))
37 import Control.Monad.IO.Class (liftIO)
38 import Data.Aeson (FromJSON, ToJSON)
40 import Data.Text (Text())
41 import Data.Time (UTCTime)
42 import GHC.Generics (Generic)
43 import Gargantext.API.Metrics
44 import Gargantext.API.Ngrams (TabType(..), TableNgramsApi, TableNgramsApiGet, tableNgramsPatch, getTableNgrams, HasRepo, QueryParamR)
45 import Gargantext.API.Ngrams.Tools
46 import Gargantext.API.Search ( SearchAPI, searchIn, SearchInQuery)
47 import Gargantext.Core.Types (Offset, Limit, ListType(..), HasInvalidError)
48 import Gargantext.Core.Types.Main (Tree, NodeTree)
49 import Gargantext.Database.Facet (FacetDoc , runViewDocuments, OrderBy(..),FacetChart,runViewAuthorsDoc)
50 import qualified Gargantext.Database.Metrics as Metrics
51 import Gargantext.Database.Metrics.NgramsByNode (getNodesByNgramsOnlyUser)
52 import Gargantext.Database.Node.Children (getChildren)
53 import Gargantext.Database.Schema.Ngrams (NgramsType(..))
54 import Gargantext.Database.Schema.Node ( getNodesWithParentId, getNode, deleteNode, deleteNodes, mkNodeWithParent, JSONB, NodeError(..), HasNodeError(..))
55 import Gargantext.Database.Schema.Node (defaultList)
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.Types.Node (CorpusId, ContactId)
60 import Gargantext.Database.Utils -- (Cmd, CmdM)
61 import Gargantext.Prelude
62 import Gargantext.API.Settings
63 import Gargantext.Text.Metrics (Scored(..))
64 import Gargantext.Viz.Graph hiding (Node)-- (Graph(_graph_metadata),LegendField(..), GraphMetadata(..),readGraphFromJson,defaultGraph)
65 import Gargantext.Viz.Graph.Tools (cooc2graph)
66 import Gargantext.Viz.Phylo.API (getPhylo)
67 import Gargantext.Viz.Phylo hiding (Tree)
69 import Test.QuickCheck (elements)
70 import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)
71 import qualified Data.Map as Map
72 import qualified Gargantext.Database.Node.Update as U (update, Update(..))
75 import qualified Gargantext.Text.List.Learn as Learn
76 import qualified Data.Vector as Vec
90 -------------------------------------------------------------------
91 -- TODO-ACCESS: access by admin only.
92 -- At first let's just have an isAdmin check.
93 -- Later: check userId CanDeleteNodes Nothing
94 -- TODO-EVENTS: DeletedNodes [NodeId]
95 -- {"tag": "DeletedNodes", "nodes": [Int*]}
96 type NodesAPI = Delete '[JSON] Int
99 -- Be careful: really delete nodes
100 -- Access by admin only
101 nodesAPI :: [NodeId] -> GargServer NodesAPI
102 nodesAPI ids = deleteNodes ids
104 ------------------------------------------------------------------------
105 -- | TODO-ACCESS: access by admin only.
106 -- At first let's just have an isAdmin check.
107 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
108 -- To manage the Users roots
111 -- TODO needs design discussion.
112 type Roots = Get '[JSON] [NodeAny]
113 :<|> Put '[JSON] Int -- TODO
115 -- | TODO: access by admin only
116 roots :: GargServer Roots
117 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
118 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
120 -------------------------------------------------------------------
121 -- | Node API Types management
122 -- TODO-ACCESS : access by users
123 -- No ownership check is needed if we strictly follow the capability model.
125 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
127 -- CanRenameNode (or part of CanEditNode?)
128 -- CanCreateChildren (PostNodeApi)
129 -- CanEditNode / CanPutNode TODO not implemented yet
131 -- CanPatch (TableNgramsApi)
134 type NodeAPI a = Get '[JSON] (Node a)
135 :<|> "rename" :> RenameApi
136 :<|> PostNodeApi -- TODO move to children POST
138 :<|> Delete '[JSON] Int
139 :<|> "children" :> ChildrenApi a
142 :<|> "table" :> TableApi
143 :<|> "list" :> TableNgramsApi
144 :<|> "listGet" :> TableNgramsApiGet
145 :<|> "pairing" :> PairingApi
148 :<|> "chart" :> ChartApi
149 :<|> "phylo" :> PhyloAPI
151 :<|> "favorites" :> FavApi
152 :<|> "documents" :> DocsApi
153 :<|> "search":> Summary "Node Search"
154 :> ReqBody '[JSON] SearchInQuery
155 :> QueryParam "offset" Int
156 :> QueryParam "limit" Int
157 :> QueryParam "order" OrderBy
159 :<|> "metrics" :> MetricsAPI
161 -- TODO-ACCESS: check userId CanRenameNode nodeId
162 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
163 type RenameApi = Summary " Rename Node"
164 :> ReqBody '[JSON] RenameNode
167 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
168 :> ReqBody '[JSON] PostNode
169 :> Post '[JSON] [NodeId]
171 type ChildrenApi a = Summary " Summary children"
172 :> QueryParam "type" NodeType
173 :> QueryParam "offset" Int
174 :> QueryParam "limit" Int
175 :> Get '[JSON] [Node a]
176 ------------------------------------------------------------------------
177 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
178 nodeAPI :: JSONB a => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
185 :<|> getChildren id p
189 :<|> tableNgramsPatch id
190 :<|> getTableNgrams id
204 ------------------------------------------------------------------------
205 data RenameNode = RenameNode { r_name :: Text }
208 instance FromJSON RenameNode
209 instance ToJSON RenameNode
210 instance ToSchema RenameNode
211 instance Arbitrary RenameNode where
212 arbitrary = elements [RenameNode "test"]
213 ------------------------------------------------------------------------
214 data PostNode = PostNode { pn_name :: Text
215 , pn_typename :: NodeType}
218 instance FromJSON PostNode
219 instance ToJSON PostNode
220 instance ToSchema PostNode
221 instance Arbitrary PostNode where
222 arbitrary = elements [PostNode "Node test" NodeCorpus]
224 ------------------------------------------------------------------------
225 type DocsApi = Summary "Docs : Move to trash"
226 :> ReqBody '[JSON] Documents
227 :> Delete '[JSON] [Int]
229 data Documents = Documents { documents :: [NodeId]}
232 instance FromJSON Documents
233 instance ToJSON Documents
234 instance ToSchema Documents
236 delDocs :: CorpusId -> Documents -> Cmd err [Int]
237 delDocs cId ds = nodesToTrash $ map (\n -> (cId, n, True)) $ documents ds
239 ------------------------------------------------------------------------
240 type FavApi = Summary " Favorites label"
241 :> ReqBody '[JSON] Favorites
243 :<|> Summary " Favorites unlabel"
244 :> ReqBody '[JSON] Favorites
245 :> Delete '[JSON] [Int]
247 data Favorites = Favorites { favorites :: [NodeId]}
250 instance FromJSON Favorites
251 instance ToJSON Favorites
252 instance ToSchema Favorites
254 putFav :: CorpusId -> Favorites -> Cmd err [Int]
255 putFav cId fs = nodesToFavorite $ map (\n -> (cId, n, True)) $ favorites fs
257 delFav :: CorpusId -> Favorites -> Cmd err [Int]
258 delFav cId fs = nodesToFavorite $ map (\n -> (cId, n, False)) $ favorites fs
260 favApi :: CorpusId -> GargServer FavApi
261 favApi cId = putFav cId :<|> delFav cId
263 ------------------------------------------------------------------------
264 type TableApi = Summary " Table API"
265 :> QueryParam "view" TabType
266 :> QueryParam "offset" Int
267 :> QueryParam "limit" Int
268 :> QueryParam "order" OrderBy
269 :> Get '[JSON] [FacetDoc]
271 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
272 type PairingApi = Summary " Pairing API"
273 :> QueryParam "view" TabType -- TODO change TabType -> DocType (CorpusId for pairing)
274 :> QueryParam "offset" Int
275 :> QueryParam "limit" Int
276 :> QueryParam "order" OrderBy
277 :> Get '[JSON] [FacetDoc]
279 ------------------------------------------------------------------------
280 type ChartApi = Summary " Chart API"
281 :> QueryParam "from" UTCTime
282 :> QueryParam "to" UTCTime
283 :> Get '[JSON] [FacetChart]
285 -- Depending on the Type of the Node, we could post
286 -- New documents for a corpus
287 -- New map list terms
288 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
290 -- To launch a query and update the corpus
291 -- :<|> "query" :> Capture "string" Text :> Get '[JSON] Text
293 ------------------------------------------------------------------------
294 -- TODO-ACCESS: CanGetNode
295 -- TODO-EVENTS: No events as this is a read only query.
296 type GraphAPI = Get '[JSON] Graph
298 graphAPI :: NodeId -> GargServer GraphAPI
300 nodeGraph <- getNode nId HyperdataGraph
302 let metadata = GraphMetadata "Title" [maybe 0 identity $ _node_parentId nodeGraph]
303 [ LegendField 1 "#FFF" "Cluster"
304 , LegendField 2 "#FFF" "Cluster"
306 -- (map (\n -> LegendField n "#FFFFFF" (pack $ show n)) [1..10])
307 let cId = maybe (panic "no parentId") identity $ _node_parentId nodeGraph
309 lId <- defaultList cId
310 ngs <- filterListWithRoot GraphTerm <$> mapTermListRoot [lId] NgramsTerms
312 myCooc <- Map.filter (>1) <$> getCoocByNgrams (Diagonal False)
313 <$> groupNodesByNgrams ngs
314 <$> getNodesByNgramsOnlyUser cId NgramsTerms (Map.keys ngs)
316 liftIO $ set graph_metadata (Just metadata) <$> cooc2graph myCooc
319 type PhyloAPI = Summary "Phylo API"
320 -- :> QueryParam "param" PhyloQueryView
321 :> Get '[JSON] PhyloView
323 phyloAPI :: NodeId -> GargServer PhyloAPI
324 phyloAPI n = pure $ getPhylo n
328 instance HasNodeError ServantErr where
329 _NodeError = prism' mk (const Nothing) -- $ panic "HasNodeError ServantErr: not a prism")
331 e = "Gargantext NodeError: "
332 mk NoListFound = err404 { errBody = e <> "No list found" }
333 mk NoRootFound = err404 { errBody = e <> "No Root found" }
334 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
335 mk NoUserFound = err404 { errBody = e <> "No User found" }
337 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
338 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
339 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
340 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
341 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
342 mk ManyParents = err500 { errBody = e <> "Too many parents" }
343 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
345 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
346 instance HasTreeError ServantErr where
347 _TreeError = prism' mk (const Nothing) -- $ panic "HasTreeError ServantErr: not a prism")
350 mk NoRoot = err404 { errBody = e <> "Root node not found" }
351 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
352 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
354 type TreeAPI = Get '[JSON] (Tree NodeTree)
355 -- TODO-ACCESS: CanTree or CanGetNode
356 -- TODO-EVENTS: No events as this is a read only query.
357 treeAPI :: NodeId -> GargServer TreeAPI
360 ------------------------------------------------------------------------
361 -- | Check if the name is less than 255 char
362 rename :: NodeId -> RenameNode -> Cmd err [Int]
363 rename nId (RenameNode name') = U.update (U.Rename nId name')
365 getTable :: NodeId -> Maybe TabType
366 -> Maybe Offset -> Maybe Limit
367 -> Maybe OrderBy -> Cmd err [FacetDoc]
368 getTable cId ft o l order =
370 (Just Docs) -> runViewDocuments cId False o l order
371 (Just Trash) -> runViewDocuments cId True o l order
372 _ -> panic "not implemented"
374 getPairing :: ContactId -> Maybe TabType
375 -> Maybe Offset -> Maybe Limit
376 -> Maybe OrderBy -> Cmd err [FacetDoc]
377 getPairing cId ft o l order =
379 (Just Docs) -> runViewAuthorsDoc cId False o l order
380 (Just Trash) -> runViewAuthorsDoc cId True o l order
381 _ -> panic "not implemented"
384 getChart :: NodeId -> Maybe UTCTime -> Maybe UTCTime
385 -> Cmd err [FacetChart]
386 getChart _ _ _ = undefined -- TODO
388 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
389 postNode uId pId (PostNode nodeName nt) = mkNodeWithParent nt (Just pId) uId nodeName
391 putNode :: NodeId -> Cmd err Int
392 putNode = undefined -- TODO
394 query :: Monad m => Text -> m Text
399 -- TODO Is it possible to adapt the function according to iValue input ?
400 --upload :: MultipartData -> Handler Text
401 --upload multipartData = do
403 -- putStrLn "Inputs:"
404 -- forM_ (inputs multipartData) $ \input ->
405 -- putStrLn $ " " <> show (iName input)
406 -- <> " -> " <> show (iValue input)
408 -- forM_ (files multipartData) $ \file -> do
409 -- content <- readFile (fdFilePath file)
410 -- putStrLn $ "Content of " <> show (fdFileName file)
411 -- <> " at " <> fdFilePath file
413 -- pure (pack "Data loaded")
415 -------------------------------------------------------------------------------
417 type MetricsAPI = Summary "SepGen IncExc metrics"
418 :> QueryParam "list" ListId
419 :> QueryParamR "ngramsType" TabType
420 :> QueryParam "limit" Int
421 :> Get '[JSON] Metrics
423 getMetrics :: NodeId -> GargServer MetricsAPI
424 getMetrics cId maybeListId tabType maybeLimit = do
425 (ngs', scores) <- Metrics.getMetrics' cId maybeListId tabType maybeLimit
428 metrics = map (\(Scored t s1 s2) -> Metric t s1 s2 (listType t ngs')) scores
429 listType t m = maybe (panic errorMsg) fst $ Map.lookup t m
430 errorMsg = "API.Node.metrics: key absent"
432 pure $ Metrics metrics