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(..), TableNgramsApi, TableNgramsApiGet, tableNgramsPatch, getTableNgrams, 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)
53 import Gargantext.Database.Facet (FacetDoc , runViewDocuments, OrderBy(..),FacetChart,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.Types.Node (CorpusId, ContactId)
60 import Gargantext.Database.Utils -- (Cmd, CmdM)
61 import Gargantext.Prelude
62 import Gargantext.Text.Metrics (Scored(..))
64 import Gargantext.Viz.Phylo hiding (Tree)
65 import Gargantext.Viz.Phylo.API (getPhylo)
67 import Test.QuickCheck (elements)
68 import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)
69 import qualified Data.Map as Map
70 import qualified Gargantext.Database.Metrics as Metrics
71 import qualified Gargantext.Database.Node.Update as U (update, Update(..))
74 import qualified Gargantext.Text.List.Learn as Learn
75 import qualified Data.Vector as Vec
79 type NodesAPI = Delete '[JSON] Int
82 -- Be careful: really delete nodes
83 -- Access by admin only
84 nodesAPI :: [NodeId] -> GargServer NodesAPI
85 nodesAPI ids = deleteNodes ids
87 ------------------------------------------------------------------------
88 -- | TODO-ACCESS: access by admin only.
89 -- At first let's just have an isAdmin check.
90 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
91 -- To manage the Users roots
94 -- TODO needs design discussion.
95 type Roots = Get '[JSON] [NodeAny]
96 :<|> Put '[JSON] Int -- TODO
98 -- | TODO: access by admin only
99 roots :: GargServer Roots
100 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
101 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
103 -------------------------------------------------------------------
104 -- | Node API Types management
105 -- TODO-ACCESS : access by users
106 -- No ownership check is needed if we strictly follow the capability model.
108 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
110 -- CanRenameNode (or part of CanEditNode?)
111 -- CanCreateChildren (PostNodeApi)
112 -- CanEditNode / CanPutNode TODO not implemented yet
114 -- CanPatch (TableNgramsApi)
117 type NodeAPI a = Get '[JSON] (Node a)
118 :<|> "rename" :> RenameApi
119 :<|> PostNodeApi -- TODO move to children POST
121 :<|> Delete '[JSON] Int
122 :<|> "children" :> ChildrenApi a
125 :<|> "table" :> TableApi
126 :<|> "list" :> TableNgramsApi
127 :<|> "listGet" :> TableNgramsApiGet
128 :<|> "pairing" :> PairingApi
131 :<|> "chart" :> ChartApi
132 :<|> "phylo" :> PhyloAPI
134 :<|> "favorites" :> FavApi
135 :<|> "documents" :> DocsApi
136 :<|> "search":> Summary "Node Search"
137 :> ReqBody '[JSON] SearchInQuery
138 :> QueryParam "offset" Int
139 :> QueryParam "limit" Int
140 :> QueryParam "order" OrderBy
142 :<|> "metrics" :> MetricsAPI
144 -- TODO-ACCESS: check userId CanRenameNode nodeId
145 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
146 type RenameApi = Summary " Rename Node"
147 :> ReqBody '[JSON] RenameNode
150 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
151 :> ReqBody '[JSON] PostNode
152 :> Post '[JSON] [NodeId]
154 type ChildrenApi a = Summary " Summary children"
155 :> QueryParam "type" NodeType
156 :> QueryParam "offset" Int
157 :> QueryParam "limit" Int
158 :> Get '[JSON] [Node a]
159 ------------------------------------------------------------------------
160 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
161 nodeAPI :: JSONB a => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
168 :<|> getChildren id p
172 :<|> tableNgramsPatch id
173 :<|> getTableNgrams id
187 ------------------------------------------------------------------------
188 data RenameNode = RenameNode { r_name :: Text }
191 instance FromJSON RenameNode
192 instance ToJSON RenameNode
193 instance ToSchema RenameNode
194 instance Arbitrary RenameNode where
195 arbitrary = elements [RenameNode "test"]
196 ------------------------------------------------------------------------
197 data PostNode = PostNode { pn_name :: Text
198 , pn_typename :: NodeType}
201 instance FromJSON PostNode
202 instance ToJSON PostNode
203 instance ToSchema PostNode
204 instance Arbitrary PostNode where
205 arbitrary = elements [PostNode "Node test" NodeCorpus]
207 ------------------------------------------------------------------------
208 type DocsApi = Summary "Docs : Move to trash"
209 :> ReqBody '[JSON] Documents
210 :> Delete '[JSON] [Int]
212 data Documents = Documents { documents :: [NodeId]}
215 instance FromJSON Documents
216 instance ToJSON Documents
217 instance ToSchema Documents
219 delDocs :: CorpusId -> Documents -> Cmd err [Int]
220 delDocs cId ds = nodesToTrash $ map (\n -> (cId, n, True)) $ documents ds
222 ------------------------------------------------------------------------
223 type FavApi = Summary " Favorites label"
224 :> ReqBody '[JSON] Favorites
226 :<|> Summary " Favorites unlabel"
227 :> ReqBody '[JSON] Favorites
228 :> Delete '[JSON] [Int]
230 data Favorites = Favorites { favorites :: [NodeId]}
233 instance FromJSON Favorites
234 instance ToJSON Favorites
235 instance ToSchema Favorites
237 putFav :: CorpusId -> Favorites -> Cmd err [Int]
238 putFav cId fs = nodesToFavorite $ map (\n -> (cId, n, True)) $ favorites fs
240 delFav :: CorpusId -> Favorites -> Cmd err [Int]
241 delFav cId fs = nodesToFavorite $ map (\n -> (cId, n, False)) $ favorites fs
243 favApi :: CorpusId -> GargServer FavApi
244 favApi cId = putFav cId :<|> delFav cId
246 ------------------------------------------------------------------------
247 type TableApi = Summary " Table API"
248 :> QueryParam "view" TabType
249 :> QueryParam "offset" Int
250 :> QueryParam "limit" Int
251 :> QueryParam "order" OrderBy
252 :> Get '[JSON] [FacetDoc]
254 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
255 type PairingApi = Summary " Pairing API"
256 :> QueryParam "view" TabType -- TODO change TabType -> DocType (CorpusId for pairing)
257 :> QueryParam "offset" Int
258 :> QueryParam "limit" Int
259 :> QueryParam "order" OrderBy
260 :> Get '[JSON] [FacetDoc]
262 ------------------------------------------------------------------------
263 type ChartApi = Summary " Chart API"
264 :> QueryParam "from" UTCTime
265 :> QueryParam "to" UTCTime
266 :> Get '[JSON] [FacetChart]
268 -- Depending on the Type of the Node, we could post
269 -- New documents for a corpus
270 -- New map list terms
271 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
273 -- To launch a query and update the corpus
274 -- :<|> "query" :> Capture "string" Text :> Get '[JSON] Text
276 ------------------------------------------------------------------------
279 type PhyloAPI = Summary "Phylo API"
280 -- :> QueryParam "param" PhyloQueryView
281 :> Get '[JSON] PhyloView
284 phyloAPI :: NodeId -> GargServer PhyloAPI
285 phyloAPI n = pure $ getPhylo n
289 instance HasNodeError ServantErr where
290 _NodeError = prism' mk (const Nothing) -- $ panic "HasNodeError ServantErr: not a prism")
292 e = "Gargantext NodeError: "
293 mk NoListFound = err404 { errBody = e <> "No list found" }
294 mk NoRootFound = err404 { errBody = e <> "No Root found" }
295 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
296 mk NoUserFound = err404 { errBody = e <> "No User found" }
298 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
299 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
300 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
301 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
302 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
303 mk ManyParents = err500 { errBody = e <> "Too many parents" }
304 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
306 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
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" }
315 type TreeAPI = Get '[JSON] (Tree NodeTree)
316 -- TODO-ACCESS: CanTree or CanGetNode
317 -- TODO-EVENTS: No events as this is a read only query.
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 getTable :: NodeId -> Maybe TabType
327 -> Maybe Offset -> Maybe Limit
328 -> Maybe OrderBy -> Cmd err [FacetDoc]
329 getTable cId ft o l order =
331 (Just Docs) -> runViewDocuments cId False o l order
332 (Just Trash) -> runViewDocuments cId True o l order
333 _ -> panic "not implemented"
335 getPairing :: ContactId -> Maybe TabType
336 -> Maybe Offset -> Maybe Limit
337 -> Maybe OrderBy -> Cmd err [FacetDoc]
338 getPairing cId ft o l order =
340 (Just Docs) -> runViewAuthorsDoc cId False o l order
341 (Just Trash) -> runViewAuthorsDoc cId True o l order
342 _ -> panic "not implemented"
345 getChart :: NodeId -> Maybe UTCTime -> Maybe UTCTime
346 -> Cmd err [FacetChart]
347 getChart _ _ _ = undefined -- TODO
349 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
350 postNode uId pId (PostNode nodeName nt) = mkNodeWithParent nt (Just pId) uId nodeName
352 putNode :: NodeId -> Cmd err Int
353 putNode = undefined -- TODO
355 query :: Monad m => Text -> m Text
360 -- TODO Is it possible to adapt the function according to iValue input ?
361 --upload :: MultipartData -> Handler Text
362 --upload multipartData = do
364 -- putStrLn "Inputs:"
365 -- forM_ (inputs multipartData) $ \input ->
366 -- putStrLn $ " " <> show (iName input)
367 -- <> " -> " <> show (iValue input)
369 -- forM_ (files multipartData) $ \file -> do
370 -- content <- readFile (fdFilePath file)
371 -- putStrLn $ "Content of " <> show (fdFileName file)
372 -- <> " at " <> fdFilePath file
374 -- pure (pack "Data loaded")
376 -------------------------------------------------------------------------------
378 type MetricsAPI = Summary "SepGen IncExc metrics"
379 :> QueryParam "list" ListId
380 :> QueryParamR "ngramsType" TabType
381 :> QueryParam "limit" Int
382 :> Get '[JSON] Metrics
384 getMetrics :: NodeId -> GargServer MetricsAPI
385 getMetrics cId maybeListId tabType maybeLimit = do
386 (ngs', scores) <- Metrics.getMetrics' cId maybeListId tabType maybeLimit
389 metrics = map (\(Scored t s1 s2) -> Metric t s1 s2 (listType t ngs')) scores
390 listType t m = maybe (panic errorMsg) fst $ Map.lookup t m
391 errorMsg = "API.Node.metrics: key absent"
393 pure $ Metrics metrics