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, HasRepoVar, HasRepoSaver)
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, mkNodeWithParent, 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)
77 (CmdM env ServantErr m, HasRepoVar env, HasRepoSaver env)
80 -------------------------------------------------------------------
81 -- TODO-ACCESS: access by admin only.
82 -- At first let's just have an isAdmin check.
83 -- Later: check userId CanDeleteNodes Nothing
84 -- TODO-EVENTS: DeletedNodes [NodeId]
85 -- {"tag": "DeletedNodes", "nodes": [Int*]}
86 type NodesAPI = Delete '[JSON] Int
89 -- Be careful: really delete nodes
90 -- Access by admin only
91 nodesAPI :: [NodeId] -> GargServer NodesAPI
92 nodesAPI ids = deleteNodes ids
94 ------------------------------------------------------------------------
95 -- | TODO-ACCESS: access by admin only.
96 -- At first let's just have an isAdmin check.
97 -- Later: CanAccessAnyNode or (CanGetAnyNode, CanPutAnyNode)
98 -- To manage the Users roots
101 -- TODO needs design discussion.
102 type Roots = Get '[JSON] [NodeAny]
103 :<|> Put '[JSON] Int -- TODO
105 -- | TODO: access by admin only
106 roots :: GargServer Roots
107 roots = (liftIO (putStrLn ( "/user" :: Text)) >> getNodesWithParentId 0 Nothing)
108 :<|> pure (panic "not implemented yet") -- TODO use patch map to update what we need
110 -------------------------------------------------------------------
111 -- | Node API Types management
112 -- TODO-ACCESS : access by users
113 -- No ownership check is needed if we strictly follow the capability model.
115 -- CanGetNode (Node, Children, TableApi, TableNgramsApiGet, PairingApi, ChartApi,
117 -- CanRenameNode (or part of CanEditNode?)
118 -- CanCreateChildren (PostNodeApi)
119 -- CanEditNode / CanPutNode TODO not implemented yet
121 -- CanPatch (TableNgramsApi)
124 type NodeAPI a = Get '[JSON] (Node a)
125 :<|> "rename" :> RenameApi
126 :<|> PostNodeApi -- TODO move to children POST
128 :<|> Delete '[JSON] Int
129 :<|> "children" :> ChildrenApi a
132 :<|> "table" :> TableApi
133 :<|> "list" :> TableNgramsApi
134 :<|> "listGet" :> TableNgramsApiGet
135 :<|> "pairing" :> PairingApi
137 :<|> "chart" :> ChartApi
138 :<|> "favorites" :> FavApi
139 :<|> "documents" :> DocsApi
140 :<|> "search":> Summary "Node Search"
141 :> ReqBody '[JSON] SearchInQuery
142 :> QueryParam "offset" Int
143 :> QueryParam "limit" Int
144 :> QueryParam "order" OrderBy
147 -- TODO-ACCESS: check userId CanRenameNode nodeId
148 -- TODO-EVENTS: NodeRenamed RenameNode or re-use some more general NodeEdited...
149 type RenameApi = Summary " Rename Node"
150 :> ReqBody '[JSON] RenameNode
153 type PostNodeApi = Summary " PostNode Node with ParentId as {id}"
154 :> ReqBody '[JSON] PostNode
155 :> Post '[JSON] [NodeId]
157 type ChildrenApi a = Summary " Summary children"
158 :> QueryParam "type" NodeType
159 :> QueryParam "offset" Int
160 :> QueryParam "limit" Int
161 :> Get '[JSON] [Node a]
162 ------------------------------------------------------------------------
163 -- TODO: make the NodeId type indexed by `a`, then we no longer need the proxy.
164 nodeAPI :: JSONB a => proxy a -> UserId -> NodeId -> GargServer (NodeAPI a)
171 :<|> getChildren id p
175 :<|> tableNgramsPatch id
176 :<|> 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] [FacetChart]
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 ------------------------------------------------------------------------
276 -- TODO-ACCESS: CanGetNode
277 -- TODO-EVENTS: No events as this is a read only query.
278 type GraphAPI = Get '[JSON] Graph
280 graphAPI :: NodeId -> GargServer GraphAPI
283 nodeGraph <- getNode nId HyperdataGraph
286 let metadata = GraphMetadata title [maybe 0 identity $ _node_parentId nodeGraph]
287 [ LegendField 1 "#FFF" "Cluster"
288 , LegendField 2 "#FFF" "Cluster"
290 -- (map (\n -> LegendField n "#FFFFFF" (pack $ show n)) [1..10])
291 let cId = maybe (panic "no parentId") identity $ _node_parentId nodeGraph
292 lId <- defaultList cId
293 myCooc <- getCoocByDocDev cId lId
294 liftIO $ set graph_metadata (Just metadata)
295 <$> cooc2graph myCooc
297 -- <$> maybe defaultGraph identity
298 -- <$> readGraphFromJson "purescript-gargantext/dist/examples/imtNew.json"
299 -- t <- textFlow (Mono EN) (Contexts contextText)
300 -- liftIO $ liftIO $ pure $ maybe t identity maybeGraph
301 -- TODO what do we get about the node? to replace contextText
303 instance HasNodeError ServantErr where
304 _NodeError = prism' mk (const Nothing) -- $ panic "HasNodeError ServantErr: not a prism")
307 mk NoListFound = err404 { errBody = e <> "No list found" }
308 mk NoRootFound = err404 { errBody = e <> "No Root found" }
309 mk NoCorpusFound = err404 { errBody = e <> "No Corpus found" }
310 mk NoUserFound = err404 { errBody = e <> "No User found" }
312 mk MkNode = err500 { errBody = e <> "Cannot mk node" }
313 mk NegativeId = err500 { errBody = e <> "Node with negative Id" }
314 mk UserNoParent = err500 { errBody = e <> "Should not have parent"}
315 mk HasParent = err500 { errBody = e <> "NodeType has parent" }
316 mk NotImplYet = err500 { errBody = e <> "Not implemented yet" }
317 mk ManyParents = err500 { errBody = e <> "Too many parents" }
318 mk ManyNodeUsers = err500 { errBody = e <> "Many userNode/user" }
320 -- TODO(orphan): There should be a proper APIError data type with a case TreeError.
321 instance HasTreeError ServantErr where
322 _TreeError = prism' mk (const Nothing) -- $ panic "HasTreeError ServantErr: not a prism")
325 mk NoRoot = err404 { errBody = e <> "Root node not found" }
326 mk EmptyRoot = err500 { errBody = e <> "Root node should not be empty" }
327 mk TooManyRoots = err500 { errBody = e <> "Too many root nodes" }
329 type TreeAPI = Get '[JSON] (Tree NodeTree)
330 -- TODO-ACCESS: CanTree or CanGetNode
331 -- TODO-EVENTS: No events as this is a read only query.
332 treeAPI :: NodeId -> GargServer TreeAPI
335 ------------------------------------------------------------------------
336 -- | Check if the name is less than 255 char
337 rename :: NodeId -> RenameNode -> Cmd err [Int]
338 rename nId (RenameNode name) = U.update (U.Rename nId name)
340 getTable :: NodeId -> Maybe TabType
341 -> Maybe Offset -> Maybe Limit
342 -> Maybe OrderBy -> Cmd err [FacetDoc]
343 getTable cId ft o l order = case ft of
344 (Just Docs) -> runViewDocuments cId False o l order
345 (Just Trash) -> runViewDocuments cId True o l order
346 _ -> panic "not implemented"
348 getPairing :: ContactId -> Maybe TabType
349 -> Maybe Offset -> Maybe Limit
350 -> Maybe OrderBy -> Cmd err [FacetDoc]
351 getPairing cId ft o l order = case ft of
352 (Just Docs) -> runViewAuthorsDoc cId False o l order
353 (Just Trash) -> runViewAuthorsDoc cId True o l order
354 _ -> panic "not implemented"
357 getChart :: NodeId -> Maybe UTCTime -> Maybe UTCTime
358 -> Cmd err [FacetChart]
359 getChart _ _ _ = undefined -- TODO
361 postNode :: HasNodeError err => UserId -> NodeId -> PostNode -> Cmd err [NodeId]
362 postNode uId pId (PostNode name nt) = mkNodeWithParent nt (Just pId) uId name
364 putNode :: NodeId -> Cmd err Int
365 putNode = undefined -- TODO
367 query :: Monad m => Text -> m Text
372 -- TODO Is it possible to adapt the function according to iValue input ?
373 --upload :: MultipartData -> Handler Text
374 --upload multipartData = do
376 -- putStrLn "Inputs:"
377 -- forM_ (inputs multipartData) $ \input ->
378 -- putStrLn $ " " <> show (iName input)
379 -- <> " -> " <> show (iValue input)
381 -- forM_ (files multipartData) $ \file -> do
382 -- content <- readFile (fdFilePath file)
383 -- putStrLn $ "Content of " <> show (fdFileName file)
384 -- <> " at " <> fdFilePath file
386 -- pure (pack "Data loaded")