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-name-shadowing -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 -------------------------------------------------------------------
25 module Gargantext.API.Node
26 ( module Gargantext.API.Node
28 , HyperdataAnnuaire(..)
30 , HyperdataResource(..)
32 , HyperdataDocument(..)
33 , HyperdataDocumentV3(..)
35 -------------------------------------------------------------------
36 import Control.Lens (prism', set)
37 import Control.Monad.IO.Class (liftIO)
38 import Control.Monad ((>>))
39 --import System.IO (putStrLn, readFile)
41 import Data.Aeson (FromJSON, ToJSON)
42 import Data.Text (Text())
44 import Data.Time (UTCTime)
46 import GHC.Generics (Generic)
49 import Gargantext.API.Ngrams (TabType(..), TableNgramsApi, TableNgramsApiGet, tableNgramsPatch, getTableNgrams)
50 import Gargantext.Prelude
51 import Gargantext.Database.Types.Node
52 import Gargantext.Database.Utils -- (Cmd, CmdM)
53 import Gargantext.Database.Schema.Node ( getNodesWithParentId, getNode, deleteNode, deleteNodes, mk, JSONB, NodeError(..), HasNodeError(..))
54 import Gargantext.Database.Node.Children (getChildren)
55 import qualified Gargantext.Database.Node.Update as U (update, Update(..))
56 import Gargantext.Database.Facet (FacetDoc , runViewDocuments, OrderBy(..),FacetChart,runViewAuthorsDoc)
57 import Gargantext.Database.Tree (treeDB, HasTreeError(..), TreeError(..))
58 import Gargantext.Database.Metrics.Count (getCoocByDocDev)
59 import Gargantext.Database.Schema.Node (defaultList)
60 import Gargantext.Database.Schema.NodeNode (nodesToFavorite, nodesToTrash)
61 import Gargantext.API.Search ( SearchAPI, searchIn, SearchInQuery)
64 import Gargantext.Text.Flow (cooc2graph)
65 import Gargantext.Viz.Graph hiding (Node)-- (Graph(_graph_metadata),LegendField(..), GraphMetadata(..),readGraphFromJson,defaultGraph)
66 -- import Gargantext.Core (Lang(..))
67 import Gargantext.Core.Types (Offset, Limit)
68 import Gargantext.Core.Types.Main (Tree, NodeTree)
69 import Gargantext.Database.Types.Node (CorpusId, ContactId)
70 -- import Gargantext.Text.Terms (TermType(..))
72 import Test.QuickCheck (elements)
73 import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)
75 type GargServer api = forall env m. CmdM env ServantErr m => ServerT api m
77 -------------------------------------------------------------------
78 -- | TODO : access by admin only
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 by admin only
89 -- To manager the Users roots
90 type Roots = Get '[JSON] [NodeAny]
91 :<|> Put '[JSON] Int -- TODO
93 -- | TODO: access by admin only
94 roots :: GargServer Roots
95 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
96 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
98 -------------------------------------------------------------------
99 -- | Node API Types management
100 -- TODO : access by users
101 type NodeAPI a = Get '[JSON] (Node a)
102 :<|> "rename" :> RenameApi
105 :<|> Delete '[JSON] Int
106 :<|> "children" :> ChildrenApi a
109 :<|> "table" :> TableApi
110 :<|> "list" :> TableNgramsApi
111 :<|> "listGet" :> TableNgramsApiGet
112 :<|> "pairing" :> PairingApi
114 :<|> "chart" :> ChartApi
115 :<|> "favorites" :> FavApi
116 :<|> "documents" :> DocsApi
117 :<|> "search":> Summary "Node Search"
118 :> ReqBody '[JSON] SearchInQuery
119 :> QueryParam "offset" Int
120 :> QueryParam "limit" Int
121 :> QueryParam "order" OrderBy
124 type RenameApi = Summary " Rename Node"
125 :> ReqBody '[JSON] RenameNode
128 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
129 :> ReqBody '[JSON] PostNode
130 :> Post '[JSON] [NodeId]
132 type ChildrenApi a = Summary " Summary children"
133 :> QueryParam "type" NodeType
134 :> QueryParam "offset" Int
135 :> QueryParam "limit" Int
136 :> Get '[JSON] [Node a]
137 ------------------------------------------------------------------------
138 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
139 nodeAPI :: JSONB a => proxy a -> NodeId -> GargServer (NodeAPI a)
140 nodeAPI p id = getNode id p
145 :<|> getChildren id p
149 :<|> tableNgramsPatch id
150 :<|> getTableNgrams id
160 ------------------------------------------------------------------------
161 data RenameNode = RenameNode { r_name :: Text }
164 instance FromJSON RenameNode
165 instance ToJSON RenameNode
166 instance ToSchema RenameNode
167 instance Arbitrary RenameNode where
168 arbitrary = elements [RenameNode "test"]
169 ------------------------------------------------------------------------
170 data PostNode = PostNode { pn_name :: Text
171 , pn_typename :: NodeType}
174 instance FromJSON PostNode
175 instance ToJSON PostNode
176 instance ToSchema PostNode
177 instance Arbitrary PostNode where
178 arbitrary = elements [PostNode "Node test" NodeCorpus]
180 ------------------------------------------------------------------------
181 type DocsApi = Summary "Docs : Move to trash"
182 :> ReqBody '[JSON] Documents
183 :> Delete '[JSON] [Int]
185 data Documents = Documents { documents :: [NodeId]}
188 instance FromJSON Documents
189 instance ToJSON Documents
190 instance ToSchema Documents
192 delDocs :: CorpusId -> Documents -> Cmd err [Int]
193 delDocs cId ds = nodesToTrash $ map (\n -> (cId, n, True)) $ documents ds
195 ------------------------------------------------------------------------
196 type FavApi = Summary " Favorites label"
197 :> ReqBody '[JSON] Favorites
199 :<|> Summary " Favorites unlabel"
200 :> ReqBody '[JSON] Favorites
201 :> Delete '[JSON] [Int]
203 data Favorites = Favorites { favorites :: [NodeId]}
206 instance FromJSON Favorites
207 instance ToJSON Favorites
208 instance ToSchema Favorites
210 putFav :: CorpusId -> Favorites -> Cmd err [Int]
211 putFav cId fs = nodesToFavorite $ map (\n -> (cId, n, True)) $ favorites fs
213 delFav :: CorpusId -> Favorites -> Cmd err [Int]
214 delFav cId fs = nodesToFavorite $ map (\n -> (cId, n, False)) $ favorites fs
216 favApi :: CorpusId -> GargServer FavApi
217 favApi cId = putFav cId :<|> delFav cId
219 ------------------------------------------------------------------------
220 type TableApi = Summary " Table API"
221 :> QueryParam "view" TabType
222 :> QueryParam "offset" Int
223 :> QueryParam "limit" Int
224 :> QueryParam "order" OrderBy
225 :> Get '[JSON] [FacetDoc]
227 -- TODO adapt FacetDoc -> ListDoc (and add type of document as column)
228 type PairingApi = Summary " Pairing API"
229 :> QueryParam "view" TabType -- TODO change TabType -> DocType (CorpusId for pairing)
230 :> QueryParam "offset" Int
231 :> QueryParam "limit" Int
232 :> QueryParam "order" OrderBy
233 :> Get '[JSON] [FacetDoc]
235 ------------------------------------------------------------------------
236 type ChartApi = Summary " Chart API"
237 :> QueryParam "from" UTCTime
238 :> QueryParam "to" UTCTime
239 :> Get '[JSON] [FacetChart]
241 -- Depending on the Type of the Node, we could post
242 -- New documents for a corpus
243 -- New map list terms
244 -- :<|> "process" :> MultipartForm MultipartData :> Post '[JSON] Text
246 -- To launch a query and update the corpus
247 -- :<|> "query" :> Capture "string" Text :> Get '[JSON] Text
249 ------------------------------------------------------------------------
250 type GraphAPI = Get '[JSON] Graph
252 graphAPI :: NodeId -> GargServer GraphAPI
255 nodeGraph <- getNode nId HyperdataGraph
257 let title = "Graph Title"
258 let metadata = GraphMetadata title [maybe 0 identity $ _node_parentId nodeGraph]
259 [ LegendField 1 "#FFF" "Cluster"
260 , LegendField 2 "#FFF" "Cluster"
262 -- (map (\n -> LegendField n "#FFFFFF" (pack $ show n)) [1..10])
263 let cId = maybe (panic "no parentId") identity $ _node_parentId nodeGraph
264 lId <- defaultList cId
265 myCooc <- getCoocByDocDev cId lId
266 liftIO $ set graph_metadata (Just metadata)
267 <$> cooc2graph myCooc
269 -- <$> maybe defaultGraph identity
270 -- <$> readGraphFromJson "purescript-gargantext/dist/examples/imtNew.json"
271 -- t <- textFlow (Mono EN) (Contexts contextText)
272 -- liftIO $ liftIO $ pure $ maybe t identity maybeGraph
273 -- TODO what do we get about the node? to replace contextText
275 instance HasNodeError ServantErr where
276 _NodeError = prism' mk (const Nothing) -- $ panic "HasNodeError ServantErr: not a prism")
279 mk NoListFound = err404 { errBody = e <> "No list found" }
280 mk NoRootFound = err404 { errBody = e <> "No Root found" }
281 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
282 mk NoUserFound = err404 { errBody = e <> "No User found" }
284 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
285 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
286 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
287 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
288 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
289 mk ManyParents = err500 { errBody = e <> "Too many parents" }
290 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
292 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
293 instance HasTreeError ServantErr where
294 _TreeError = prism' mk (const Nothing) -- $ panic "HasTreeError ServantErr: not a prism")
297 mk NoRoot = err404 { errBody = e <> "Root node not found" }
298 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
299 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
301 type TreeAPI = Get '[JSON] (Tree NodeTree)
302 treeAPI :: NodeId -> GargServer TreeAPI
305 ------------------------------------------------------------------------
306 -- | Check if the name is less than 255 char
307 rename :: NodeId -> RenameNode -> Cmd err [Int]
308 rename nId (RenameNode name) = U.update (U.Rename nId name)
310 getTable :: NodeId -> Maybe TabType
311 -> Maybe Offset -> Maybe Limit
312 -> Maybe OrderBy -> Cmd err [FacetDoc]
313 getTable cId ft o l order = case ft of
314 (Just Docs) -> runViewDocuments cId False o l order
315 (Just Trash) -> runViewDocuments cId True o l order
316 _ -> panic "not implemented"
318 getPairing :: ContactId -> Maybe TabType
319 -> Maybe Offset -> Maybe Limit
320 -> Maybe OrderBy -> Cmd err [FacetDoc]
321 getPairing cId ft o l order = case ft of
322 (Just Docs) -> runViewAuthorsDoc cId False o l order
323 (Just Trash) -> runViewAuthorsDoc cId True o l order
324 _ -> panic "not implemented"
327 getChart :: NodeId -> Maybe UTCTime -> Maybe UTCTime
328 -> Cmd err [FacetChart]
329 getChart _ _ _ = undefined -- TODO
331 postNode :: NodeId -> PostNode -> Cmd err [NodeId]
332 postNode pId (PostNode name nt) = mk nt (Just pId) name
334 putNode :: NodeId -> Cmd err Int
335 putNode = undefined -- TODO
337 query :: Monad m => Text -> m Text
342 -- TODO Is it possible to adapt the function according to iValue input ?
343 --upload :: MultipartData -> Handler Text
344 --upload multipartData = do
346 -- putStrLn "Inputs:"
347 -- forM_ (inputs multipartData) $ \input ->
348 -- putStrLn $ " " <> show (iName input)
349 -- <> " -> " <> show (iValue input)
351 -- forM_ (files multipartData) $ \file -> do
352 -- content <- readFile (fdFilePath file)
353 -- putStrLn $ "Content of " <> show (fdFileName file)
354 -- <> " at " <> fdFilePath file
356 -- pure (pack "Data loaded")