2 Module : Gargantext.API
3 Description : REST API declaration
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 Main REST API of Gargantext (both Server and Client sides)
11 Thanks @yannEsposito for our discussions at the beginning of this project :).
15 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
17 {-# LANGUAGE NoImplicitPrelude #-}
18 {-# LANGUAGE DataKinds #-}
19 {-# LANGUAGE DeriveGeneric #-}
20 {-# LANGUAGE FlexibleContexts #-}
21 {-# LANGUAGE FlexibleInstances #-}
22 {-# LANGUAGE OverloadedStrings #-}
23 {-# LANGUAGE TemplateHaskell #-}
24 {-# LANGUAGE TypeOperators #-}
25 {-# LANGUAGE KindSignatures #-}
26 {-# LANGUAGE RankNTypes #-}
27 {-# LANGUAGE ScopedTypeVariables #-}
28 {-# LANGUAGE TypeFamilies #-}
29 {-# LANGUAGE UndecidableInstances #-}
31 ---------------------------------------------------------------------
34 ---------------------------------------------------------------------
36 import System.IO (FilePath)
38 import GHC.Generics (D1, Meta (..), Rep)
39 import GHC.TypeLits (AppendSymbol, Symbol)
42 import Control.Exception (finally)
43 import Control.Monad.Except (withExceptT, ExceptT)
44 import Control.Monad.IO.Class (liftIO)
45 import Control.Monad.Reader (ReaderT, runReaderT)
46 import Data.Aeson.Encode.Pretty (encodePretty)
47 import qualified Data.ByteString.Lazy.Char8 as BL8
49 import Data.Text (Text)
50 import qualified Data.Text.IO as T
51 --import qualified Data.Set as Set
55 import Network.Wai.Handler.Warp hiding (defaultSettings)
58 import Servant.HTML.Blaze (HTML)
59 --import Servant.Mock (mock)
60 --import Servant.Job.Server (WithCallbacks)
61 import Servant.Static.TH.Internal.Server (fileTreeToServer)
62 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
63 import Servant.Swagger
64 import Servant.Swagger.UI
65 -- import Servant.API.Stream
66 import Text.Blaze.Html (Html)
68 --import Gargantext.API.Swagger
70 --import Gargantext.Database.Node.Contact (HyperdataContact)
71 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth)
72 import Gargantext.API.Count ( CountAPI, count, Query)
73 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
74 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo, ApiNgramsTableDoc, apiNgramsTableDoc)
75 import Gargantext.API.Node
76 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
77 import Gargantext.API.Types
78 import Gargantext.API.Upload
79 import Gargantext.Core.Types (HasInvalidError(..))
80 import Gargantext.Database.Facet
81 import Gargantext.Database.Schema.Node (HasNodeError(..), NodeError)
82 import Gargantext.Database.Tree (HasTreeError(..), TreeError)
83 import Gargantext.Database.Types.Node
84 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
85 import Gargantext.Database.Utils (HasConnection)
86 import Gargantext.Prelude
87 import Gargantext.Viz.Graph.API
89 --import Gargantext.API.Orchestrator
90 --import Gargantext.API.Orchestrator.Types
92 ---------------------------------------------------------------------
94 import GHC.Base (Applicative)
95 -- import Control.Lens
97 import Data.List (lookup)
98 import Data.Text.Encoding (encodeUtf8)
100 --import Network.Wai (Request, requestHeaders, responseLBS)
101 import Network.Wai (Request, requestHeaders)
102 --import qualified Network.Wai.Handler.Warp as Warp
103 import Network.Wai.Middleware.Cors
105 import Network.Wai.Middleware.RequestLogger
106 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
108 import Network.HTTP.Types hiding (Query)
111 import Gargantext.API.Settings
114 = GargNodeError NodeError
115 | GargTreeError TreeError
116 | GargInvalidError Validation
119 makePrisms ''GargError
121 instance HasNodeError GargError where
122 _NodeError = _GargNodeError
124 instance HasInvalidError GargError where
125 _InvalidError = _GargInvalidError
127 instance HasTreeError GargError where
128 _TreeError = _GargTreeError
130 showAsServantErr :: Show a => a -> ServantErr
131 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
133 fireWall :: Applicative f => Request -> FireWall -> f Bool
135 let origin = lookup "Origin" (requestHeaders req)
136 let host = lookup "Host" (requestHeaders req)
138 let hostOk = Just (encodeUtf8 "localhost:3000")
139 let originOk = Just (encodeUtf8 "http://localhost:8008")
141 if origin == originOk
143 || (not $ unFireWall fw)
149 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
150 makeMockApp :: MockEnv -> IO Application
152 let serverApp = appMock
154 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
155 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
156 let checkOriginAndHost app req resp = do
157 blocking <- fireWall req (env ^. menv_firewall)
160 False -> resp ( responseLBS status401 []
161 "Invalid Origin or Host header")
163 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
164 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
165 { corsOrigins = Nothing -- == /*
166 , corsMethods = [ methodGet , methodPost , methodPut
167 , methodDelete, methodOptions, methodHead]
168 , corsRequestHeaders = ["authorization", "content-type"]
169 , corsExposedHeaders = Nothing
170 , corsMaxAge = Just ( 60*60*24 ) -- one day
171 , corsVaryOrigin = False
172 , corsRequireOrigin = False
173 , corsIgnoreFailures = False
176 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
177 -- $ Warp.defaultSettings
179 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
180 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
184 makeDevMiddleware :: IO Middleware
185 makeDevMiddleware = do
187 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
188 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
189 -- let checkOriginAndHost app req resp = do
190 -- blocking <- fireWall req (env ^. menv_firewall)
192 -- True -> app req resp
193 -- False -> resp ( responseLBS status401 []
194 -- "Invalid Origin or Host header")
196 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
197 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
198 { corsOrigins = Nothing -- == /*
199 , corsMethods = [ methodGet , methodPost , methodPut
200 , methodDelete, methodOptions, methodHead]
201 , corsRequestHeaders = ["authorization", "content-type"]
202 , corsExposedHeaders = Nothing
203 , corsMaxAge = Just ( 60*60*24 ) -- one day
204 , corsVaryOrigin = False
205 , corsRequireOrigin = False
206 , corsIgnoreFailures = False
209 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
210 -- $ Warp.defaultSettings
212 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
213 pure $ logStdoutDev . corsMiddleware
215 ---------------------------------------------------------------------
218 -- | API for serving @swagger.json@
219 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
221 -- | API for serving main operational routes of @gargantext.org@
224 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
225 -- | TODO :<|> Summary "Latest API" :> GargAPI'
228 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
232 "auth" :> Summary "AUTH API"
233 :> ReqBody '[JSON] AuthRequest
234 :> Post '[JSON] AuthResponse
237 :<|> "user" :> Summary "First user endpoint"
241 :<|> "node" :> Summary "Node endpoint"
242 :> Capture "id" NodeId :> NodeAPI HyperdataAny
245 :<|> "corpus":> Summary "Corpus endpoint"
246 :> Capture "id" CorpusId :> NodeAPI HyperdataCorpus
249 :<|> "annuaire":> Summary "Annuaire endpoint"
250 :> Capture "id" AnnuaireId :> NodeAPI HyperdataAnnuaire
253 :<|> "document":> Summary "Document endpoint"
254 :> Capture "id" DocId :> "ngrams" :> ApiNgramsTableDoc
257 :<|> "nodes" :> Summary "Nodes endpoint"
258 :> ReqBody '[JSON] [NodeId] :> NodesAPI
260 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
262 :<|> "count" :> Summary "Count endpoint"
263 :> ReqBody '[JSON] Query :> CountAPI
266 :<|> "search":> Summary "Search endpoint"
267 :> ReqBody '[JSON] SearchQuery
268 :> QueryParam "offset" Int
269 :> QueryParam "limit" Int
270 :> QueryParam "order" OrderBy
273 -- TODO move to NodeAPI?
274 :<|> "graph" :> Summary "Graph endpoint"
275 :> Capture "id" NodeId :> GraphAPI
277 -- TODO move to NodeAPI?
279 :<|> "tree" :> Summary "Tree endpoint"
280 :> Capture "id" NodeId :> TreeAPI
282 :<|> "upload" :> ApiUpload
285 -- :<|> "scraper" :> WithCallbacks ScraperAPI
291 -- :<|> "list" :> Capture "id" Int :> NodeAPI
292 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
293 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
294 ---------------------------------------------------------------------
295 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
297 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
299 ---------------------------------------------------------------------
300 -- | Server declarations
302 server :: forall env. (HasConnection env, HasRepo env, HasSettings env)
303 => env -> IO (Server API)
305 -- orchestrator <- scrapyOrchestrator env
307 :<|> hoistServer (Proxy :: Proxy GargAPI) transform serverGargAPI
310 transform :: forall a. ReaderT env (ExceptT GargError IO) a -> Handler a
311 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
313 serverGargAPI :: GargServer GargAPI
314 serverGargAPI -- orchestrator
317 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) fakeUserId
318 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) fakeUserId
319 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) fakeUserId
320 :<|> apiNgramsTableDoc
322 :<|> count -- TODO: undefined
324 :<|> graphAPI -- TODO: mock
329 fakeUserId = 1 -- TODO
331 serverStatic :: Server (Get '[HTML] Html)
333 let path = "purescript-gargantext/dist/index.html"
334 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
338 ---------------------------------------------------------------------
339 swaggerFront :: Server SwaggerFrontAPI
340 swaggerFront = schemaUiServer swaggerDoc
343 --gargMock :: Server GargAPI
344 --gargMock = mock apiGarg Proxy
346 ---------------------------------------------------------------------
347 makeApp :: (HasConnection env, HasRepo env, HasSettings env)
348 => env -> IO Application
349 makeApp = fmap (serve api) . server
351 --appMock :: Application
352 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
354 ---------------------------------------------------------------------
358 apiGarg :: Proxy GargAPI
360 ---------------------------------------------------------------------
362 schemaUiServer :: (Server api ~ Handler Swagger)
363 => Swagger -> Server (SwaggerSchemaUI' dir api)
364 schemaUiServer = swaggerSchemaUIServer
367 -- Type Family for the Documentation
368 type family TypeName (x :: *) :: Symbol where
370 TypeName Text = "Text"
371 TypeName x = GenericTypeName x (Rep x ())
373 type family GenericTypeName t (r :: *) :: Symbol where
374 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
376 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
379 -- | Swagger Specifications
380 swaggerDoc :: Swagger
381 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
382 & info.title .~ "Gargantext"
383 & info.version .~ "4.0.2" -- TODO same version as Gargantext
384 -- & info.base_url ?~ (URL "http://gargantext.org/")
385 & info.description ?~ "REST API specifications"
386 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
387 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
388 ["Gargantext" & description ?~ "Main operations"]
389 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
391 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
393 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
394 swaggerWriteJSON :: IO ()
395 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
397 portRouteInfo :: PortNumber -> IO ()
398 portRouteInfo port = do
399 T.putStrLn " ----Main Routes----- "
400 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
401 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
403 stopGargantext :: HasRepoSaver env => env -> IO ()
404 stopGargantext env = do
405 T.putStrLn "----- Stopping gargantext -----"
406 runReaderT saveRepo env
408 -- | startGargantext takes as parameters port number and Ini file.
409 startGargantext :: PortNumber -> FilePath -> IO ()
410 startGargantext port file = do
411 env <- newEnv port file
414 mid <- makeDevMiddleware
415 run port (mid app) `finally` stopGargantext env
418 startGargantextMock :: PortNumber -> IO ()
419 startGargantextMock port = do
421 application <- makeMockApp . MockEnv $ FireWall False