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(..),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
65 import Test.QuickCheck (elements)
66 import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)
67 import qualified Data.Map as Map
68 import qualified Gargantext.Database.Metrics as Metrics
69 import qualified Gargantext.Database.Node.Update as U (update, Update(..))
72 import qualified Gargantext.Text.List.Learn as Learn
73 import qualified Data.Vector as Vec
77 type NodesAPI = Delete '[JSON] Int
80 -- Be careful: really delete nodes
81 -- Access by admin only
82 nodesAPI :: [NodeId] -> GargServer NodesAPI
83 nodesAPI ids = deleteNodes ids
85 ------------------------------------------------------------------------
86 -- | TODO-ACCESS: access by admin only.
87 -- At first let's just have an isAdmin check.
88 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
89 -- To manage the Users roots
92 -- TODO needs design discussion.
93 type Roots = Get '[JSON] [NodeAny]
94 :<|> Put '[JSON] Int -- TODO
96 -- | TODO: access by admin only
97 roots :: GargServer Roots
98 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
99 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
101 -------------------------------------------------------------------
102 -- | Node API Types management
103 -- TODO-ACCESS : access by users
104 -- No ownership check is needed if we strictly follow the capability model.
106 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
108 -- CanRenameNode (or part of CanEditNode?)
109 -- CanCreateChildren (PostNodeApi)
110 -- CanEditNode / CanPutNode TODO not implemented yet
112 -- CanPatch (TableNgramsApi)
115 type NodeAPI a = Get '[JSON] (Node a)
116 :<|> "rename" :> RenameApi
117 :<|> PostNodeApi -- TODO move to children POST
119 :<|> Delete '[JSON] Int
120 :<|> "children" :> ChildrenApi a
123 :<|> "table" :> TableApi
124 :<|> "list" :> TableNgramsApi
125 :<|> "listGet" :> TableNgramsApiGet
126 :<|> "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
141 :<|> "phylo" :> PhyloAPI
143 -- TODO-ACCESS: check userId CanRenameNode nodeId
144 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
145 type RenameApi = Summary " Rename Node"
146 :> ReqBody '[JSON] RenameNode
149 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
150 :> ReqBody '[JSON] PostNode
151 :> Post '[JSON] [NodeId]
153 type ChildrenApi a = Summary " Summary children"
154 :> QueryParam "type" NodeType
155 :> QueryParam "offset" Int
156 :> QueryParam "limit" Int
157 :> Get '[JSON] [Node a]
158 ------------------------------------------------------------------------
159 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
160 nodeAPI :: JSONB a => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
167 :<|> getChildren id p
171 :<|> tableNgramsPatch id
172 :<|> getTableNgrams id
186 ------------------------------------------------------------------------
187 data RenameNode = RenameNode { r_name :: Text }
190 instance FromJSON RenameNode
191 instance ToJSON RenameNode
192 instance ToSchema RenameNode
193 instance Arbitrary RenameNode where
194 arbitrary = elements [RenameNode "test"]
195 ------------------------------------------------------------------------
196 data PostNode = PostNode { pn_name :: Text
197 , pn_typename :: NodeType}
200 instance FromJSON PostNode
201 instance ToJSON PostNode
202 instance ToSchema PostNode
203 instance Arbitrary PostNode where
204 arbitrary = elements [PostNode "Node test" NodeCorpus]
206 ------------------------------------------------------------------------
207 type DocsApi = Summary "Docs : Move to trash"
208 :> ReqBody '[JSON] Documents
209 :> Delete '[JSON] [Int]
211 data Documents = Documents { documents :: [NodeId]}
214 instance FromJSON Documents
215 instance ToJSON Documents
216 instance ToSchema Documents
218 delDocs :: CorpusId -> Documents -> Cmd err [Int]
219 delDocs cId ds = nodesToTrash $ map (\n -> (cId, n, True)) $ documents ds
221 ------------------------------------------------------------------------
222 type FavApi = Summary " Favorites label"
223 :> ReqBody '[JSON] Favorites
225 :<|> Summary " Favorites unlabel"
226 :> ReqBody '[JSON] Favorites
227 :> Delete '[JSON] [Int]
229 data Favorites = Favorites { favorites :: [NodeId]}
232 instance FromJSON Favorites
233 instance ToJSON Favorites
234 instance ToSchema Favorites
236 putFav :: CorpusId -> Favorites -> Cmd err [Int]
237 putFav cId fs = nodesToFavorite $ map (\n -> (cId, n, True)) $ favorites fs
239 delFav :: CorpusId -> Favorites -> Cmd err [Int]
240 delFav cId fs = nodesToFavorite $ map (\n -> (cId, n, False)) $ favorites fs
242 favApi :: CorpusId -> GargServer FavApi
243 favApi cId = putFav cId :<|> delFav cId
245 ------------------------------------------------------------------------
246 type TableApi = Summary " Table API"
247 :> QueryParam "view" TabType
248 :> QueryParam "offset" Int
249 :> QueryParam "limit" Int
250 :> QueryParam "order" OrderBy
251 :> Get '[JSON] [FacetDoc]
253 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
254 type PairingApi = Summary " Pairing API"
255 :> QueryParam "view" TabType -- TODO change TabType -> DocType (CorpusId for pairing)
256 :> QueryParam "offset" Int
257 :> QueryParam "limit" Int
258 :> QueryParam "order" OrderBy
259 :> Get '[JSON] [FacetDoc]
261 ------------------------------------------------------------------------
262 type ChartApi = Summary " Chart API"
263 :> QueryParam "from" UTCTime
264 :> QueryParam "to" UTCTime
265 :> Get '[JSON] (ChartMetrics Histo)
267 -- Depending on the Type of the Node, we could post
268 -- New documents for a corpus
269 -- New map list terms
270 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
272 -- To launch a query and update the corpus
273 -- :<|> "query" :> Capture "string" Text :> Get '[JSON] Text
275 ------------------------------------------------------------------------
278 instance HasNodeError ServantErr where
279 _NodeError = prism' mk (const Nothing) -- $ panic "HasNodeError ServantErr: not a prism")
281 e = "Gargantext NodeError: "
282 mk NoListFound = err404 { errBody = e <> "No list found" }
283 mk NoRootFound = err404 { errBody = e <> "No Root found" }
284 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
285 mk NoUserFound = err404 { errBody = e <> "No User found" }
287 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
288 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
289 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
290 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
291 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
292 mk ManyParents = err500 { errBody = e <> "Too many parents" }
293 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
295 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
296 instance HasTreeError ServantErr where
297 _TreeError = prism' mk (const Nothing) -- $ panic "HasTreeError ServantErr: not a prism")
300 mk NoRoot = err404 { errBody = e <> "Root node not found" }
301 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
302 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
304 type TreeAPI = Get '[JSON] (Tree NodeTree)
305 -- TODO-ACCESS: CanTree or CanGetNode
306 -- TODO-EVENTS: No events as this is a read only query.
307 treeAPI :: NodeId -> GargServer TreeAPI
310 ------------------------------------------------------------------------
311 -- | Check if the name is less than 255 char
312 rename :: NodeId -> RenameNode -> Cmd err [Int]
313 rename nId (RenameNode name') = U.update (U.Rename nId name')
315 getTable :: NodeId -> Maybe TabType
316 -> Maybe Offset -> Maybe Limit
317 -> Maybe OrderBy -> Cmd err [FacetDoc]
318 getTable cId ft o l order =
320 (Just Docs) -> runViewDocuments cId False o l order
321 (Just Trash) -> runViewDocuments cId True o l order
322 _ -> panic "not implemented"
324 getPairing :: ContactId -> Maybe TabType
325 -> Maybe Offset -> Maybe Limit
326 -> Maybe OrderBy -> Cmd err [FacetDoc]
327 getPairing cId ft o l order =
329 (Just Docs) -> runViewAuthorsDoc cId False o l order
330 (Just Trash) -> runViewAuthorsDoc cId True o l order
331 _ -> panic "not implemented"
333 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
334 postNode uId pId (PostNode nodeName nt) = mkNodeWithParent nt (Just pId) uId nodeName
336 putNode :: NodeId -> Cmd err Int
337 putNode = undefined -- TODO
339 query :: Monad m => Text -> m Text
344 -- TODO Is it possible to adapt the function according to iValue input ?
345 --upload :: MultipartData -> Handler Text
346 --upload multipartData = do
348 -- putStrLn "Inputs:"
349 -- forM_ (inputs multipartData) $ \input ->
350 -- putStrLn $ " " <> show (iName input)
351 -- <> " -> " <> show (iValue input)
353 -- forM_ (files multipartData) $ \file -> do
354 -- content <- readFile (fdFilePath file)
355 -- putStrLn $ "Content of " <> show (fdFileName file)
356 -- <> " at " <> fdFilePath file
358 -- pure (pack "Data loaded")
360 -------------------------------------------------------------------------------
362 type MetricsAPI = Summary "SepGen IncExc metrics"
363 :> QueryParam "list" ListId
364 :> QueryParamR "ngramsType" TabType
365 :> QueryParam "limit" Int
366 :> Get '[JSON] Metrics
368 getMetrics :: NodeId -> GargServer MetricsAPI
369 getMetrics cId maybeListId tabType maybeLimit = do
370 (ngs', scores) <- Metrics.getMetrics' cId maybeListId tabType maybeLimit
373 metrics = map (\(Scored t s1 s2) -> Metric t s1 s2 (listType t ngs')) scores
374 listType t m = maybe (panic errorMsg) fst $ Map.lookup t m
375 errorMsg = "API.Node.metrics: key absent"
377 pure $ Metrics metrics