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 TemplateHaskell #-}
21 {-# LANGUAGE ConstraintKinds #-}
23 module Gargantext.Core.NodeStory where
25 -- import Debug.Trace (traceShow)
26 import Codec.Serialise (serialise, deserialise)
27 import Codec.Serialise.Class
28 import Control.Concurrent (MVar(), withMVar, newMVar, modifyMVar_)
29 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
30 import Control.Lens (makeLenses, Getter, (^.))
31 import Control.Monad.Except
32 import Control.Monad.Reader
33 import Data.Aeson hiding ((.=), decode)
34 import Data.Map.Strict (Map)
37 import Database.PostgreSQL.Simple.FromField (FromField(fromField), fromJSONField)
38 import GHC.Generics (Generic)
39 import Gargantext.API.Ngrams.Types
40 import Gargantext.Core.Types (NodeId)
41 import Gargantext.Core.Utils.Prefix (unPrefix)
42 import Gargantext.Database.Prelude (CmdM', HasConnectionPool, HasConfig)
43 import Gargantext.Database.Query.Table.Node.Error (HasNodeError())
44 import Gargantext.Prelude
45 import Opaleye (DefaultFromField(..), SqlJsonb, fromPGSFromField)
46 import System.Directory (renameFile, createDirectoryIfMissing, doesFileExist, removeFile)
47 import System.IO (FilePath, hClose)
48 import System.IO.Temp (withTempFile)
49 import qualified Data.ByteString.Lazy as DBL
50 import qualified Data.List as List
51 import qualified Data.Map.Strict as Map
52 import qualified Gargantext.Database.Query.Table.Ngrams as TableNgrams
54 ------------------------------------------------------------------------
55 data NodeStoryEnv = NodeStoryEnv
56 { _nse_var :: !(MVar NodeListStory)
57 , _nse_saver :: !(IO ())
58 , _nse_getter :: [NodeId] -> IO (MVar NodeListStory)
59 --, _nse_cleaner :: !(IO ()) -- every 12 hours: cleans the repos of unused NodeStories
60 -- , _nse_lock :: !FileLock -- TODO (it depends on the option: if with database or file only)
64 type HasNodeStory env err m = ( CmdM' env err m
69 , HasConnectionPool env
73 class (HasNodeStoryVar env, HasNodeStorySaver env)
74 => HasNodeStoryEnv env where
75 hasNodeStory :: Getter env NodeStoryEnv
77 class HasNodeStoryVar env where
78 hasNodeStoryVar :: Getter env ([NodeId] -> IO (MVar NodeListStory))
80 class HasNodeStorySaver env where
81 hasNodeStorySaver :: Getter env (IO ())
83 ------------------------------------------------------------------------
84 readNodeStoryEnv :: NodeStoryDir -> IO NodeStoryEnv
85 readNodeStoryEnv nsd = do
86 mvar <- nodeStoryVar nsd Nothing []
87 saver <- mkNodeStorySaver nsd mvar
88 pure $ NodeStoryEnv { _nse_var = mvar
90 , _nse_getter = nodeStoryVar nsd (Just mvar) }
92 ------------------------------------------------------------------------
93 mkNodeStorySaver :: NodeStoryDir -> MVar NodeListStory -> IO (IO ())
94 mkNodeStorySaver nsd mvns = mkDebounce settings
96 settings = defaultDebounceSettings
97 { debounceAction = withMVar mvns (writeNodeStories nsd)
98 , debounceFreq = 1 * minute
99 -- , debounceEdge = trailingEdge -- Trigger on the trailing edge
102 second = 10^(6 :: Int)
104 nodeStoryVar :: NodeStoryDir
105 -> Maybe (MVar NodeListStory)
107 -> IO (MVar NodeListStory)
108 nodeStoryVar nsd Nothing ni = nodeStoryIncs nsd Nothing ni >>= newMVar
109 nodeStoryVar nsd (Just mv) ni = do
110 _ <- modifyMVar_ mv $ \mv' -> (nodeStoryIncs nsd (Just mv') ni)
114 nodeStoryInc :: NodeStoryDir -> Maybe NodeListStory -> NodeId -> IO NodeListStory
115 nodeStoryInc nsd (Just ns@(NodeStory nls)) ni = do
116 case Map.lookup ni nls of
118 (NodeStory nls') <- nodeStoryRead nsd ni
119 pure $ NodeStory $ Map.union nls nls'
121 nodeStoryInc nsd Nothing ni = nodeStoryRead nsd ni
124 nodeStoryIncs :: NodeStoryDir
125 -> Maybe NodeListStory
128 nodeStoryIncs _ Nothing [] = pure $ NodeStory $ Map.empty
129 nodeStoryIncs nsd (Just nls) ns = foldM (\m n -> nodeStoryInc nsd (Just m) n) nls ns
130 nodeStoryIncs nsd Nothing (ni:ns) = do
131 m <- nodeStoryRead nsd ni
132 nodeStoryIncs nsd (Just m) ns
135 nodeStoryDec :: NodeStoryDir
139 nodeStoryDec nsd ns@(NodeStory nls) ni = do
140 case Map.lookup ni nls of
142 -- we make sure the corresponding file repo is really removed
143 _ <- nodeStoryRemove nsd ni
146 let ns' = Map.filterWithKey (\k _v -> k /= ni) nls
147 _ <- nodeStoryRemove nsd ni
151 nodeStoryRead :: NodeStoryDir -> NodeId -> IO NodeListStory
152 nodeStoryRead nsd ni = do
153 _repoDir <- createDirectoryIfMissing True nsd
154 let nsp = nodeStoryPath nsd ni
155 exists <- doesFileExist nsp
157 then deserialise <$> DBL.readFile nsp
158 else pure (initNodeStory ni)
160 nodeStoryRemove :: NodeStoryDir -> NodeId -> IO ()
161 nodeStoryRemove nsd ni = do
162 let nsp = nodeStoryPath nsd ni
163 exists <- doesFileExist nsp
170 nodeStoryRead_test :: NodeStoryDir -> NodeId -> IO (Maybe [ TableNgrams.NgramsType ])
171 nodeStoryRead_test nsd ni = nodeStoryRead nsd ni >>= \n -> pure
177 ------------------------------------------------------------------------
178 type NodeStoryDir = FilePath
180 writeNodeStories :: NodeStoryDir -> NodeListStory -> IO ()
181 writeNodeStories fp nls = do
182 _done <- mapM (writeNodeStory fp) $ splitByNode nls
183 -- printDebug "[writeNodeStories]" done
186 writeNodeStory :: NodeStoryDir -> (NodeId, NodeListStory) -> IO ()
187 writeNodeStory rdfp (n, ns) = saverAction' rdfp n ns
189 splitByNode :: NodeListStory -> [(NodeId, NodeListStory)]
190 splitByNode (NodeStory m) =
191 List.map (\(n,a) -> (n, NodeStory $ Map.singleton n a)) $ Map.toList m
194 saverAction' :: Serialise a => NodeStoryDir -> NodeId -> a -> IO ()
195 saverAction' repoDir nId a = do
196 withTempFile repoDir ((cs $ show nId) <> "-tmp-repo.cbor") $ \fp h -> do
197 -- printDebug "[repoSaverAction]" fp
198 DBL.hPut h $ serialise a
200 renameFile fp (nodeStoryPath repoDir nId)
202 nodeStoryPath :: NodeStoryDir -> NodeId -> FilePath
203 nodeStoryPath repoDir nId = repoDir <> "/" <> filename
205 filename = "repo" <> "-" <> (cs $ show nId) <> ".cbor"
208 ------------------------------------------------------------------------
209 -- TODO : repo Migration TODO TESTS
211 repoMigration :: NodeStoryDir -> NgramsRepo -> IO ()
212 repoMigration fp r = writeNodeStories fp (repoToNodeListStory r)
214 repoToNodeListStory :: NgramsRepo -> NodeListStory
215 repoToNodeListStory (Repo _v s h) = NodeStory $ Map.fromList ns
217 s' = ngramsState_migration s
218 h' = ngramsStatePatch_migration h
219 ns = List.map (\(n,ns')
220 -> (n, let hs = fromMaybe [] (Map.lookup n h') in
221 Archive { _a_version = List.length hs
227 ngramsState_migration :: NgramsState
228 -> Map NodeId NgramsState'
229 ngramsState_migration ns =
230 Map.fromListWith (Map.union) $
233 -> map (\(nid, table)
234 -> (nid, Map.singleton nt table)
235 ) $ Map.toList nTable
239 ngramsStatePatch_migration :: [NgramsStatePatch]
240 -> Map NodeId [NgramsStatePatch']
241 ngramsStatePatch_migration np' = Map.fromListWith (<>)
245 toPatch :: NgramsStatePatch -> [(NodeId, [NgramsStatePatch'])]
249 -> map (\(nid, table)
250 -> (nid, [fst $ Patch.singleton nt table])
251 ) $ Patch.toList nTable
254 ------------------------------------------------------------------------
256 {- | Node Story for each NodeType where the Key of the Map is NodeId
257 TODO : generalize for any NodeType, let's start with NodeList which
258 is implemented already
260 data NodeStory s p = NodeStory { _unNodeStory :: Map NodeId (Archive s p) }
261 deriving (Generic, Show)
263 instance (FromJSON s, FromJSON p) => FromJSON (NodeStory s p)
264 instance (ToJSON s, ToJSON p) => ToJSON (NodeStory s p)
265 instance (Serialise s, Serialise p) => Serialise (NodeStory s p)
267 data Archive s p = Archive
268 { _a_version :: !Version
271 -- first patch in the list is the most recent
273 deriving (Generic, Show)
275 instance (Serialise s, Serialise p) => Serialise (Archive s p)
278 type NodeListStory = NodeStory NgramsState' NgramsStatePatch'
280 type NgramsState' = Map TableNgrams.NgramsType NgramsTableMap
281 type NgramsStatePatch' = PatchMap TableNgrams.NgramsType NgramsTablePatch
282 instance Serialise NgramsStatePatch'
283 instance FromField (Archive NgramsState' NgramsStatePatch')
285 fromField = fromJSONField
286 instance DefaultFromField SqlJsonb (Archive NgramsState' NgramsStatePatch')
288 defaultFromField = fromPGSFromField
290 -- TODO Semigroup instance for unions
292 instance (Semigroup s, Semigroup p) => Semigroup (Archive s p) where
293 (<>) (Archive { _a_history = p }) (Archive { _a_version = v'
295 , _a_history = p'}) =
296 Archive { _a_version = v'
298 , _a_history = p' <> p }
300 instance Monoid (Archive NgramsState' NgramsStatePatch') where
301 mempty = Archive { _a_version = 0
305 instance (FromJSON s, FromJSON p) => FromJSON (Archive s p) where
306 parseJSON = genericParseJSON $ unPrefix "_a_"
308 instance (ToJSON s, ToJSON p) => ToJSON (Archive s p) where
309 toJSON = genericToJSON $ unPrefix "_a_"
310 toEncoding = genericToEncoding $ unPrefix "_a_"
312 ------------------------------------------------------------------------
313 initNodeStory :: Monoid s => NodeId -> NodeStory s p
314 initNodeStory ni = NodeStory $ Map.singleton ni initArchive
316 initArchive :: Monoid s => Archive s p
317 initArchive = Archive { _a_version = 0
321 initNodeListStoryMock :: NodeListStory
322 initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive
325 archive = Archive { _a_version = 0
326 , _a_state = ngramsTableMap
328 ngramsTableMap = Map.singleton TableNgrams.NgramsTerms
330 [ (n ^. ne_ngrams, ngramsElementToRepo n)
331 | n <- mockTable ^. _NgramsTable
334 ------------------------------------------------------------------------
337 ------------------------------------------------------------------------
338 -- | Lenses at the bottom of the file because Template Haskell would reorder order of execution in others cases
339 makeLenses ''NodeStoryEnv
340 makeLenses ''NodeStory