2 Module : Gargantext.Core.NodeStory
3 Description : Node API generation
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 A Node Story is a Map between NodeId and an Archive (with state,
11 version and history) for that node.
19 {-# OPTIONS_GHC -fno-warn-orphans #-}
20 {-# LANGUAGE Arrows #-}
21 {-# LANGUAGE ConstraintKinds #-}
22 {-# LANGUAGE QuasiQuotes #-}
23 {-# LANGUAGE TemplateHaskell #-}
25 module Gargantext.Core.NodeStory
36 , initNodeListStoryMock
55 -- import Debug.Trace (traceShow)
56 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
57 import Codec.Serialise.Class
58 import Control.Arrow (returnA)
59 import Control.Concurrent (MVar(), withMVar, newMVar, modifyMVar_)
60 import Control.Exception (catch, throw, SomeException(..))
61 import Control.Lens (makeLenses, Getter, (^.))
62 import Control.Monad.Except
63 import Control.Monad.Reader
64 import Data.Aeson hiding ((.=), decode)
65 import Data.ByteString.Char8 (hPutStrLn)
66 import Data.Map.Strict (Map)
68 import Data.Pool (Pool, withResource)
70 import qualified Database.PostgreSQL.Simple as PGS
71 import Database.PostgreSQL.Simple.SqlQQ (sql)
72 import Database.PostgreSQL.Simple.FromField (FromField(fromField), fromJSONField)
73 import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
74 import GHC.Generics (Generic)
75 import Gargantext.API.Ngrams.Types
76 import Gargantext.Core.Types (NodeId(..), NodeType)
77 import Gargantext.Core.Utils.Prefix (unPrefix)
78 import Gargantext.Database.Admin.Config (nodeTypeId)
79 import Gargantext.Database.Prelude (CmdM', HasConnectionPool, HasConfig)
80 import Gargantext.Database.Query.Table.Node.Error (HasNodeError())
81 import Gargantext.Prelude
82 import Opaleye (Column, DefaultFromField(..), Insert(..), Select, SqlInt4, SqlJsonb, Table, Update(..), (.==), fromPGSFromField, rCount, restrict, runInsert, runSelect, runUpdate, selectTable, sqlInt4, sqlValueJSONB, tableField, updateEasy)
83 import Opaleye.Internal.Table (Table(..))
84 import System.IO (stderr)
85 import qualified Data.Map.Strict as Map
86 import qualified Gargantext.Database.Query.Table.Ngrams as TableNgrams
88 ------------------------------------------------------------------------
89 data NodeStoryEnv = NodeStoryEnv
90 { _nse_var :: !(MVar NodeListStory)
91 , _nse_saver :: !(IO ())
92 , _nse_getter :: [NodeId] -> IO (MVar NodeListStory)
93 --, _nse_cleaner :: !(IO ()) -- every 12 hours: cleans the repos of unused NodeStories
94 -- , _nse_lock :: !FileLock -- TODO (it depends on the option: if with database or file only)
98 type HasNodeStory env err m = ( CmdM' env err m
101 , HasNodeStoryEnv env
103 , HasConnectionPool env
107 class (HasNodeStoryVar env, HasNodeStorySaver env)
108 => HasNodeStoryEnv env where
109 hasNodeStory :: Getter env NodeStoryEnv
111 class HasNodeStoryVar env where
112 hasNodeStoryVar :: Getter env ([NodeId] -> IO (MVar NodeListStory))
114 class HasNodeStorySaver env where
115 hasNodeStorySaver :: Getter env (IO ())
117 ------------------------------------------------------------------------
119 {- | Node Story for each NodeType where the Key of the Map is NodeId
120 TODO : generalize for any NodeType, let's start with NodeList which
121 is implemented already
123 data NodeStory s p = NodeStory { _unNodeStory :: Map NodeId (Archive s p) }
124 deriving (Generic, Show)
126 instance (FromJSON s, FromJSON p) => FromJSON (NodeStory s p)
127 instance (ToJSON s, ToJSON p) => ToJSON (NodeStory s p)
128 instance (Serialise s, Serialise p) => Serialise (NodeStory s p)
130 data Archive s p = Archive
131 { _a_version :: !Version
134 -- first patch in the list is the most recent
136 deriving (Generic, Show)
138 instance (Serialise s, Serialise p) => Serialise (Archive s p)
141 type NodeListStory = NodeStory NgramsState' NgramsStatePatch'
143 type NgramsState' = Map TableNgrams.NgramsType NgramsTableMap
144 type NgramsStatePatch' = PatchMap TableNgrams.NgramsType NgramsTablePatch
145 instance Serialise NgramsStatePatch'
146 instance FromField (Archive NgramsState' NgramsStatePatch')
148 fromField = fromJSONField
149 instance DefaultFromField SqlJsonb (Archive NgramsState' NgramsStatePatch')
151 defaultFromField = fromPGSFromField
153 -- TODO Semigroup instance for unions
155 instance (Semigroup s, Semigroup p) => Semigroup (Archive s p) where
156 (<>) (Archive { _a_history = p }) (Archive { _a_version = v'
158 , _a_history = p'}) =
159 Archive { _a_version = v'
161 , _a_history = p' <> p }
163 instance Monoid (Archive NgramsState' NgramsStatePatch') where
164 mempty = Archive { _a_version = 0
168 instance (FromJSON s, FromJSON p) => FromJSON (Archive s p) where
169 parseJSON = genericParseJSON $ unPrefix "_a_"
171 instance (ToJSON s, ToJSON p) => ToJSON (Archive s p) where
172 toJSON = genericToJSON $ unPrefix "_a_"
173 toEncoding = genericToEncoding $ unPrefix "_a_"
175 ------------------------------------------------------------------------
176 initNodeStory :: Monoid s => NodeId -> NodeStory s p
177 initNodeStory ni = NodeStory $ Map.singleton ni initArchive
179 initArchive :: Monoid s => Archive s p
180 initArchive = Archive { _a_version = 0
184 initNodeListStoryMock :: NodeListStory
185 initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive
188 archive = Archive { _a_version = 0
189 , _a_state = ngramsTableMap
191 ngramsTableMap = Map.singleton TableNgrams.NgramsTerms
193 [ (n ^. ne_ngrams, ngramsElementToRepo n)
194 | n <- mockTable ^. _NgramsTable
197 ------------------------------------------------------------------------
200 ------------------------------------------------------------------------
201 -- | Lenses at the bottom of the file because Template Haskell would reorder order of execution in others cases
202 makeLenses ''NodeStoryEnv
203 makeLenses ''NodeStory
206 -----------------------------------------
209 data NodeStoryPoly a b = NodeStoryDB { node_id :: a
213 type ArchiveQ = Archive NgramsState' NgramsStatePatch'
215 type NodeStoryWrite = NodeStoryPoly (Column SqlInt4) (Column SqlJsonb)
216 type NodeStoryRead = NodeStoryPoly (Column SqlInt4) (Column SqlJsonb)
218 $(makeAdaptorAndInstance "pNodeStory" ''NodeStoryPoly)
221 runPGSQuery :: (PGS.FromRow r, PGS.ToRow q) => Pool PGS.Connection -> PGS.Query -> q -> IO [r]
222 runPGSQuery pool q a = withResource pool $ \c -> catch (PGS.query c q a) (printError c)
224 printError c (SomeException e) = do
225 q' <- PGS.formatQuery c q a
227 throw (SomeException e)
230 nodeExists :: Pool PGS.Connection -> NodeId -> IO Bool
231 nodeExists pool nId = (== [PGS.Only True])
232 <$> runPGSQuery pool [sql|SELECT true FROM nodes WHERE id = ? AND ? |] (nId, True)
234 getNodesIdWithType :: Pool PGS.Connection -> NodeType -> IO [NodeId]
235 getNodesIdWithType pool nt = do
236 --ns <- withResource pool $ \c -> runSelect c $ selectNodesIdWithType nt
237 ns <- runPGSQuery pool query (nodeTypeId nt, True)
238 pure $ map (\(PGS.Only nId) -> NodeId nId) ns
239 --pure (map NodeId ns)
242 query = [sql|SELECT id FROM nodes WHERE typename = ? AND ? |]
246 nodeStoryTable :: Table NodeStoryRead NodeStoryWrite
249 ( pNodeStory NodeStoryDB { node_id = tableField "node_id"
250 , archive = tableField "archive" } )
252 nodeStorySelect :: Select NodeStoryRead
253 nodeStorySelect = selectTable nodeStoryTable
255 getNodeStory :: Pool PGS.Connection -> NodeId -> IO NodeListStory
256 getNodeStory pool (NodeId nodeId) = do
257 res <- withResource pool $ \c -> runSelect c query
258 pure $ NodeStory $ Map.fromListWith (<>) $ (\(NodeStoryDB nId a) -> (nId, a)) <$> res
260 query :: Select NodeStoryRead
261 query = proc () -> do
262 row@(NodeStoryDB node_id _) <- nodeStorySelect -< ()
263 restrict -< node_id .== sqlInt4 nodeId
266 insertNodeArchive :: Pool PGS.Connection -> NodeId -> ArchiveQ -> IO Int64
267 insertNodeArchive pool (NodeId nId) a = withResource pool $ \c -> runInsert c insert
269 insert = Insert { iTable = nodeStoryTable
270 , iRows = [NodeStoryDB { node_id = sqlInt4 nId
271 , archive = sqlValueJSONB a }]
272 , iReturning = rCount
273 , iOnConflict = Nothing }
275 updateNodeArchive :: Pool PGS.Connection -> NodeId -> ArchiveQ -> IO Int64
276 updateNodeArchive pool (NodeId nId) a = withResource pool $ \c -> runUpdate c update
278 update = Update { uTable = nodeStoryTable
279 , uUpdateWith = updateEasy (\(NodeStoryDB { .. }) -> NodeStoryDB { archive = sqlValueJSONB a, .. })
280 , uWhere = (\row -> node_id row .== sqlInt4 nId)
281 , uReturning = rCount }
283 -- nodeStoryRemove :: Pool PGS.Connection -> NodeId -> IO Int64
284 -- nodeStoryRemove pool (NodeId nId) = withResource pool $ \c -> runDelete c delete
286 -- delete = Delete { dTable = nodeStoryTable
287 -- , dWhere = (\row -> node_id row .== sqlInt4 nId)
288 -- , dReturning = rCount }
290 upsertNodeArchive :: Pool PGS.Connection -> NodeId -> ArchiveQ -> IO Int64
291 upsertNodeArchive pool nId a = do
292 (NodeStory m) <- getNodeStory pool nId
293 case Map.lookup nId m of
294 Nothing -> insertNodeArchive pool nId a
295 Just _ -> updateNodeArchive pool nId a
297 writeNodeStories :: Pool PGS.Connection -> NodeListStory -> IO ()
298 writeNodeStories pool (NodeStory nls) = do
299 _ <- mapM (\(nId, a) -> upsertNodeArchive pool nId a) $ Map.toList nls
302 -- | Returns a `NodeListStory`, updating the given one for given `NodeId`
303 nodeStoryInc :: Pool PGS.Connection -> Maybe NodeListStory -> NodeId -> IO NodeListStory
304 nodeStoryInc pool Nothing nId = getNodeStory pool nId
305 nodeStoryInc pool (Just ns@(NodeStory nls)) nId = do
306 case Map.lookup nId nls of
308 (NodeStory nls') <- getNodeStory pool nId
309 pure $ NodeStory $ Map.union nls nls'
312 nodeStoryIncs :: Pool PGS.Connection -> Maybe NodeListStory -> [NodeId] -> IO NodeListStory
313 nodeStoryIncs _ Nothing [] = pure $ NodeStory $ Map.empty
314 nodeStoryIncs pool (Just nls) ns = foldM (\m n -> nodeStoryInc pool (Just m) n) nls ns
315 nodeStoryIncs pool Nothing (ni:ns) = do
316 m <- getNodeStory pool ni
317 nodeStoryIncs pool (Just m) ns
319 -- nodeStoryDec :: Pool PGS.Connection -> NodeListStory -> NodeId -> IO NodeListStory
320 -- nodeStoryDec pool ns@(NodeStory nls) ni = do
321 -- case Map.lookup ni nls of
323 -- _ <- nodeStoryRemove pool ni
326 -- let ns' = Map.filterWithKey (\k _v -> k /= ni) nls
327 -- _ <- nodeStoryRemove pool ni
328 -- pure $ NodeStory ns'
329 ------------------------------------
331 readNodeStoryEnv :: Pool PGS.Connection -> IO NodeStoryEnv
332 readNodeStoryEnv pool = do
333 mvar <- nodeStoryVar pool Nothing []
334 saver <- mkNodeStorySaver pool mvar
335 -- let saver = modifyMVar_ mvar $ \mv' -> do
336 -- writeNodeStories mv'
338 pure $ NodeStoryEnv { _nse_var = mvar
340 , _nse_getter = nodeStoryVar pool (Just mvar) }
342 nodeStoryVar :: Pool PGS.Connection -> Maybe (MVar NodeListStory) -> [NodeId] -> IO (MVar NodeListStory)
343 nodeStoryVar pool Nothing nIds = do
344 state <- nodeStoryIncs pool Nothing nIds
346 nodeStoryVar pool (Just mv) nIds = do
347 _ <- modifyMVar_ mv $ \nsl -> (nodeStoryIncs pool (Just nsl) nIds)
350 -- TODO No debounce since this is IO stuff.
351 -- debounce is useful since it could delay the saving to some later
352 -- time, asynchronously and we keep operating on memory only.
353 mkNodeStorySaver :: Pool PGS.Connection -> MVar NodeListStory -> IO (IO ())
354 mkNodeStorySaver pool mvns = mkDebounce settings
356 settings = defaultDebounceSettings
357 { debounceAction = withMVar mvns (\ns -> writeNodeStories pool ns)
358 , debounceFreq = 1*minute
361 second = 10^(6 :: Int)
362 --mkNodeStorySaver pool mvns = withMVar mvns $ writeNodeStories pool
364 -- mkNodeStorySaver :: MVar NodeListStory -> Cmd err (Cmd err ())
365 -- mkNodeStorySaver mvns = mkDebounce settings
367 -- settings = defaultDebounceSettings
368 -- { debounceAction = withMVar mvns (\ns -> writeNodeStories ns)
369 -- , debounceFreq = 1 * minute
370 -- -- , debounceEdge = trailingEdge -- Trigger on the trailing edge
372 -- minute = 60 * second
373 -- second = 10^(6 :: Int)
376 -----------------------------------------