Commit a3dc2f3f authored by Alexandre Delanoë's avatar Alexandre Delanoë

[FIX] repo migration fixed (WIP)

parent 2b1c8e4e
...@@ -121,6 +121,7 @@ library: ...@@ -121,6 +121,7 @@ library:
- case-insensitive - case-insensitive
- cassava - cassava
- cereal # (IGraph) - cereal # (IGraph)
- cborg
- conduit - conduit
- conduit-extra - conduit-extra
- containers - containers
......
...@@ -14,6 +14,7 @@ import Servant.Job.Async (HasJobEnv(..), Job) ...@@ -14,6 +14,7 @@ import Servant.Job.Async (HasJobEnv(..), Job)
import System.Log.FastLogger import System.Log.FastLogger
import qualified Servant.Job.Core import qualified Servant.Job.Core
import Gargantext.API.Ngrams.Types (HasRepoVar(..), HasRepoSaver(..), HasRepo(..), RepoEnv(..))
import Gargantext.API.Admin.Types import Gargantext.API.Admin.Types
import Gargantext.API.Admin.Orchestrator.Types import Gargantext.API.Admin.Orchestrator.Types
import Gargantext.Database.Prelude (HasConnectionPool(..), HasConfig(..)) import Gargantext.Database.Prelude (HasConnectionPool(..), HasConfig(..))
...@@ -25,6 +26,7 @@ data Env = Env ...@@ -25,6 +26,7 @@ data Env = Env
{ _env_settings :: !Settings { _env_settings :: !Settings
, _env_logger :: !LoggerSet , _env_logger :: !LoggerSet
, _env_pool :: !(Pool Connection) , _env_pool :: !(Pool Connection)
, _env_repo :: !RepoEnv
, _env_nodeStory :: !NodeStoryEnv , _env_nodeStory :: !NodeStoryEnv
, _env_manager :: !Manager , _env_manager :: !Manager
, _env_self_url :: !BaseUrl , _env_self_url :: !BaseUrl
...@@ -53,6 +55,14 @@ instance HasNodeStorySaver Env where ...@@ -53,6 +55,14 @@ instance HasNodeStorySaver Env where
instance HasSettings Env where instance HasSettings Env where
settings = env_settings settings = env_settings
-- Specific to Repo
instance HasRepoVar Env where
repoVar = repoEnv . repoVar
instance HasRepoSaver Env where
repoSaver = repoEnv . repoSaver
instance HasRepo Env where
repoEnv = env_repo
...@@ -71,9 +81,9 @@ makeLenses ''MockEnv ...@@ -71,9 +81,9 @@ makeLenses ''MockEnv
data DevEnv = DevEnv data DevEnv = DevEnv
{ _dev_env_settings :: !Settings { _dev_env_settings :: !Settings
, _dev_env_repo :: !RepoEnv
, _dev_env_config :: !GargConfig , _dev_env_config :: !GargConfig
, _dev_env_pool :: !(Pool Connection) , _dev_env_pool :: !(Pool Connection)
, _dev_env_nodeStory :: !NodeStoryEnv , _dev_env_nodeStory :: !NodeStoryEnv
...@@ -91,7 +101,6 @@ instance HasSettings DevEnv where ...@@ -91,7 +101,6 @@ instance HasSettings DevEnv where
settings = dev_env_settings settings = dev_env_settings
instance HasNodeStoryEnv DevEnv where instance HasNodeStoryEnv DevEnv where
hasNodeStory = dev_env_nodeStory hasNodeStory = dev_env_nodeStory
...@@ -101,3 +110,12 @@ instance HasNodeStoryVar DevEnv where ...@@ -101,3 +110,12 @@ instance HasNodeStoryVar DevEnv where
instance HasNodeStorySaver DevEnv where instance HasNodeStorySaver DevEnv where
hasNodeStorySaver = hasNodeStory . nse_saver hasNodeStorySaver = hasNodeStory . nse_saver
instance HasRepoVar DevEnv where
repoVar = repoEnv . repoVar
instance HasRepoSaver DevEnv where
repoSaver = repoEnv . repoSaver
instance HasRepo DevEnv where
repoEnv = dev_env_repo
...@@ -18,32 +18,36 @@ TODO-SECURITY: Critical ...@@ -18,32 +18,36 @@ TODO-SECURITY: Critical
module Gargantext.API.Admin.Settings module Gargantext.API.Admin.Settings
where where
import Codec.Serialise (Serialise(), serialise{-, deserialise-}) import Codec.Serialise (Serialise(), serialise, deserialise)
-- import Control.Concurrent import Control.Concurrent
-- import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction) import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
import Control.Lens import Control.Lens
import Control.Monad.Logger import Control.Monad.Logger
import Control.Monad.Reader import Control.Monad.Reader
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Pool (Pool, createPool) import Data.Pool (Pool, createPool)
import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo) import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
import Gargantext.API.Admin.EnvTypes
import Gargantext.API.Admin.Types
import Gargantext.Core.NodeStory import Gargantext.Core.NodeStory
import Gargantext.Database.Prelude (databaseParameters)
import Gargantext.Prelude
import Gargantext.Prelude.Config (GargConfig(..), {-gc_repofilepath,-} readConfig) import Gargantext.Prelude.Config (GargConfig(..), {-gc_repofilepath,-} readConfig)
import Network.HTTP.Client.TLS (newTlsManager) import Network.HTTP.Client.TLS (newTlsManager)
import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey) import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
import Servant.Client (parseBaseUrl) import Servant.Client (parseBaseUrl)
import Servant.Job.Async (newJobEnv, defaultSettings) import Servant.Job.Async (newJobEnv, defaultSettings)
import System.Directory import System.Directory
-- import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive)) import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
import System.IO (FilePath, hClose) import System.IO (FilePath, hClose)
import System.IO.Temp (withTempFile) import System.IO.Temp (withTempFile)
import System.Log.FastLogger import System.Log.FastLogger
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import Gargantext.API.Admin.EnvTypes
import Gargantext.API.Admin.Types
import Gargantext.API.Ngrams.Types (NgramsRepo, HasRepo(..), RepoEnv(..), r_version, initRepo, renv_var, renv_lock)
import Gargantext.Database.Prelude (databaseParameters, HasConfig(..))
import Gargantext.Prelude
import Gargantext.Prelude.Config (gc_repofilepath)
devSettings :: FilePath -> IO Settings devSettings :: FilePath -> IO Settings
devSettings jwkFile = do devSettings jwkFile = do
jwkExists <- doesFileExist jwkFile jwkExists <- doesFileExist jwkFile
...@@ -109,7 +113,7 @@ repoSaverAction repoDir a = do ...@@ -109,7 +113,7 @@ repoSaverAction repoDir a = do
{- --{-
-- The use of mkDebounce makes sure that repoSaverAction is not called too often. -- The use of mkDebounce makes sure that repoSaverAction is not called too often.
-- If repoSaverAction start taking more time than the debounceFreq then it should -- If repoSaverAction start taking more time than the debounceFreq then it should
-- be increased. -- be increased.
...@@ -158,7 +162,7 @@ readRepoEnv repoDir = do ...@@ -158,7 +162,7 @@ readRepoEnv repoDir = do
-- TODO save in DB here -- TODO save in DB here
saver <- mkRepoSaver repoDir mvar saver <- mkRepoSaver repoDir mvar
pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock } pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
-} --}
devJwkFile :: FilePath devJwkFile :: FilePath
devJwkFile = "dev.jwk" devJwkFile = "dev.jwk"
...@@ -174,7 +178,7 @@ newEnv port file = do ...@@ -174,7 +178,7 @@ newEnv port file = do
self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port
dbParam <- databaseParameters file dbParam <- databaseParameters file
pool <- newPool dbParam pool <- newPool dbParam
-- repo <- readRepoEnv (_gc_repofilepath config_env) repo <- readRepoEnv (_gc_repofilepath config_env)
nodeStory_env <- readNodeStoryEnv (_gc_repofilepath config_env) nodeStory_env <- readNodeStoryEnv (_gc_repofilepath config_env)
scrapers_env <- newJobEnv defaultSettings manager_env scrapers_env <- newJobEnv defaultSettings manager_env
logger <- newStderrLoggerSet defaultBufSize logger <- newStderrLoggerSet defaultBufSize
...@@ -183,7 +187,7 @@ newEnv port file = do ...@@ -183,7 +187,7 @@ newEnv port file = do
{ _env_settings = settings' { _env_settings = settings'
, _env_logger = logger , _env_logger = logger
, _env_pool = pool , _env_pool = pool
-- , _env_repo = repo , _env_repo = repo
, _env_nodeStory = nodeStory_env , _env_nodeStory = nodeStory_env
, _env_manager = manager_env , _env_manager = manager_env
, _env_scrapers = scrapers_env , _env_scrapers = scrapers_env
...@@ -194,10 +198,10 @@ newEnv port file = do ...@@ -194,10 +198,10 @@ newEnv port file = do
newPool :: ConnectInfo -> IO (Pool Connection) newPool :: ConnectInfo -> IO (Pool Connection)
newPool param = createPool (connect param) close 1 (60*60) 8 newPool param = createPool (connect param) close 1 (60*60) 8
{- --{-
cleanEnv :: (HasConfig env, HasRepo env) => env -> IO () cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
cleanEnv env = do cleanEnv env = do
r <- takeMVar (env ^. repoEnv . renv_var) r <- takeMVar (env ^. repoEnv . renv_var)
repoSaverAction (env ^. hasConfig . gc_repofilepath) r repoSaverAction (env ^. hasConfig . gc_repofilepath) r
unlockFile (env ^. repoEnv . renv_lock) unlockFile (env ^. repoEnv . renv_lock)
-} --}
...@@ -31,8 +31,7 @@ type IniPath = FilePath ...@@ -31,8 +31,7 @@ type IniPath = FilePath
withDevEnv :: IniPath -> (DevEnv -> IO a) -> IO a withDevEnv :: IniPath -> (DevEnv -> IO a) -> IO a
withDevEnv iniPath k = do withDevEnv iniPath k = do
env <- newDevEnv env <- newDevEnv
k env k env `finally` cleanEnv env
-- k env `finally` cleanEnv env
where where
newDevEnv = do newDevEnv = do
...@@ -40,9 +39,11 @@ withDevEnv iniPath k = do ...@@ -40,9 +39,11 @@ withDevEnv iniPath k = do
dbParam <- databaseParameters iniPath dbParam <- databaseParameters iniPath
nodeStory_env <- readNodeStoryEnv (_gc_repofilepath cfg) nodeStory_env <- readNodeStoryEnv (_gc_repofilepath cfg)
pool <- newPool dbParam pool <- newPool dbParam
repo <- readRepoEnv (_gc_repofilepath cfg)
setts <- devSettings devJwkFile setts <- devSettings devJwkFile
pure $ DevEnv pure $ DevEnv
{ _dev_env_pool = pool { _dev_env_pool = pool
, _dev_env_repo = repo
, _dev_env_nodeStory = nodeStory_env , _dev_env_nodeStory = nodeStory_env
, _dev_env_settings = setts , _dev_env_settings = setts
, _dev_env_config = cfg , _dev_env_config = cfg
......
...@@ -36,6 +36,13 @@ mergeNgramsElement _neOld neNew = neNew ...@@ -36,6 +36,13 @@ mergeNgramsElement _neOld neNew = neNew
type RootTerm = NgramsTerm type RootTerm = NgramsTerm
getRepo :: RepoCmdM env err m => m NgramsRepo
getRepo = do
v <- view repoVar
liftBase $ readMVar v
getRepo' :: HasNodeStory env err m getRepo' :: HasNodeStory env err m
=> [ListId] -> m NodeListStory => [ListId] -> m NodeListStory
getRepo' listIds = do getRepo' listIds = do
......
...@@ -12,7 +12,7 @@ module Gargantext.API.Ngrams.Types where ...@@ -12,7 +12,7 @@ module Gargantext.API.Ngrams.Types where
import Codec.Serialise (Serialise()) import Codec.Serialise (Serialise())
import Control.Category ((>>>)) import Control.Category ((>>>))
import Control.Concurrent import Control.Concurrent
import Control.Lens (makeLenses, makePrisms, Iso', iso, from, (.~), (?=), (#), to, folded, {-withIndex, ifolded,-} view, use, (^.), (^?), (%~), (.~), (%=), at, _Just, Each(..), itraverse_, both, forOf_, (?~)) import Control.Lens (makeLenses, makePrisms, Iso', iso, from, (.~), (?=), (#), to, folded, {-withIndex, ifolded,-} view, use, (^.), (^?), (%~), (.~), (%=), at, _Just, Each(..), itraverse_, both, forOf_, (?~), Getter)
import Control.Monad.State import Control.Monad.State
import Data.Aeson hiding ((.=)) import Data.Aeson hiding ((.=))
import Data.Aeson.TH (deriveJSON) import Data.Aeson.TH (deriveJSON)
...@@ -33,7 +33,7 @@ import GHC.Generics (Generic) ...@@ -33,7 +33,7 @@ import GHC.Generics (Generic)
import Gargantext.Core.Text (size) import Gargantext.Core.Text (size)
import Gargantext.Core.Types (ListType(..), ListId, NodeId, TODO) import Gargantext.Core.Types (ListType(..), ListId, NodeId, TODO)
import Gargantext.Core.Utils.Prefix (unPrefix, unPrefixUntagged, unPrefixSwagger, wellNamedSchema) import Gargantext.Core.Utils.Prefix (unPrefix, unPrefixUntagged, unPrefixSwagger, wellNamedSchema)
import Gargantext.Database.Prelude (fromField') import Gargantext.Database.Prelude (fromField', HasConnectionPool, HasConfig, CmdM')
import Gargantext.Prelude import Gargantext.Prelude
import Gargantext.Prelude.Crypto.Hash (IsHashable(..)) import Gargantext.Prelude.Crypto.Hash (IsHashable(..))
import Protolude (maybeToEither) import Protolude (maybeToEither)
...@@ -719,7 +719,6 @@ data RepoEnv = RepoEnv ...@@ -719,7 +719,6 @@ data RepoEnv = RepoEnv
makeLenses ''RepoEnv makeLenses ''RepoEnv
{-
type RepoCmdM env err m = type RepoCmdM env err m =
( CmdM' env err m ( CmdM' env err m
, HasRepo env , HasRepo env
...@@ -744,7 +743,6 @@ instance HasRepoVar RepoEnv where ...@@ -744,7 +743,6 @@ instance HasRepoVar RepoEnv where
repoVar = renv_var repoVar = renv_var
instance HasRepoSaver RepoEnv where instance HasRepoSaver RepoEnv where
repoSaver = renv_saver repoSaver = renv_saver
-}
------------------------------------------------------------------------ ------------------------------------------------------------------------
......
...@@ -15,13 +15,15 @@ Portability : POSIX ...@@ -15,13 +15,15 @@ Portability : POSIX
module Gargantext.Core.NodeStory where module Gargantext.Core.NodeStory where
import Codec.Serialise (Serialise(), serialise, deserialise) -- import Debug.Trace (traceShow)
import Codec.Serialise (serialise, deserialise)
import Codec.Serialise.Class
import Control.Concurrent (MVar(), withMVar, newMVar) import Control.Concurrent (MVar(), withMVar, newMVar)
import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction) import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
import Control.Lens (makeLenses, Getter, (^.)) import Control.Lens (makeLenses, Getter, (^.))
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.Reader import Control.Monad.Reader
import Data.Aeson hiding ((.=)) import Data.Aeson hiding ((.=), decode)
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Monoid import Data.Monoid
...@@ -36,7 +38,7 @@ import Gargantext.Prelude ...@@ -36,7 +38,7 @@ import Gargantext.Prelude
import System.Directory (renameFile, createDirectoryIfMissing, doesFileExist) import System.Directory (renameFile, createDirectoryIfMissing, doesFileExist)
import System.IO (FilePath, hClose) import System.IO (FilePath, hClose)
import System.IO.Temp (withTempFile) import System.IO.Temp (withTempFile)
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as DBL
import qualified Data.List as List import qualified Data.List as List
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import qualified Data.Map.Strict.Patch.Internal as Patch import qualified Data.Map.Strict.Patch.Internal as Patch
...@@ -84,7 +86,7 @@ mkNodeStorySaver nsd mvns = mkDebounce settings ...@@ -84,7 +86,7 @@ mkNodeStorySaver nsd mvns = mkDebounce settings
where where
settings = defaultDebounceSettings settings = defaultDebounceSettings
{ debounceAction = withMVar mvns (writeNodeStories nsd) { debounceAction = withMVar mvns (writeNodeStories nsd)
, debounceFreq = 10 * minute , debounceFreq = 1 * minute
-- , debounceEdge = trailingEdge -- Trigger on the trailing edge -- , debounceEdge = trailingEdge -- Trigger on the trailing edge
} }
minute = 60 * second minute = 60 * second
...@@ -117,9 +119,16 @@ nodeStoryRead nsd ni = do ...@@ -117,9 +119,16 @@ nodeStoryRead nsd ni = do
let nsp = nodeStoryPath nsd ni let nsp = nodeStoryPath nsd ni
exists <- doesFileExist nsp exists <- doesFileExist nsp
if exists if exists
then deserialise <$> L.readFile nsp then deserialise <$> DBL.readFile nsp
else pure (initNodeStory ni) else pure (initNodeStory ni)
nodeStoryRead_test :: NodeStoryDir -> NodeId -> IO (Maybe [ TableNgrams.NgramsType ])
nodeStoryRead_test nsd ni = nodeStoryRead nsd ni >>= \n -> pure
$ fmap Map.keys
$ fmap _a_state
$ Map.lookup ni
$ _unNodeStory n
------------------------------------------------------------------------ ------------------------------------------------------------------------
type NodeStoryDir = FilePath type NodeStoryDir = FilePath
...@@ -140,7 +149,7 @@ saverAction' :: NodeStoryDir -> NodeId -> Serialise a => a -> IO () ...@@ -140,7 +149,7 @@ saverAction' :: NodeStoryDir -> NodeId -> Serialise a => a -> IO ()
saverAction' repoDir nId a = do saverAction' repoDir nId a = do
withTempFile repoDir ((cs $ show nId) <> "-tmp-repo.cbor") $ \fp h -> do withTempFile repoDir ((cs $ show nId) <> "-tmp-repo.cbor") $ \fp h -> do
printDebug "repoSaverAction" fp printDebug "repoSaverAction" fp
L.hPut h $ serialise a DBL.hPut h $ serialise a
hClose h hClose h
renameFile fp (nodeStoryPath repoDir nId) renameFile fp (nodeStoryPath repoDir nId)
...@@ -150,7 +159,6 @@ nodeStoryPath repoDir nId = repoDir <> "/" <> filename ...@@ -150,7 +159,6 @@ nodeStoryPath repoDir nId = repoDir <> "/" <> filename
filename = "repo" <> "-" <> (cs $ show nId) <> ".cbor" filename = "repo" <> "-" <> (cs $ show nId) <> ".cbor"
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- TODO : repo Migration TODO TESTS -- TODO : repo Migration TODO TESTS
repoMigration :: NodeStoryDir -> NgramsRepo -> IO () repoMigration :: NodeStoryDir -> NgramsRepo -> IO ()
...@@ -165,24 +173,34 @@ repoToNodeListStory (Repo _v s h) = NodeStory $ Map.fromList ns ...@@ -165,24 +173,34 @@ repoToNodeListStory (Repo _v s h) = NodeStory $ Map.fromList ns
-> (n, let hs = fromMaybe [] (Map.lookup n h') in -> (n, let hs = fromMaybe [] (Map.lookup n h') in
Archive (List.length hs) ns' hs Archive (List.length hs) ns' hs
) )
) s' ) $ Map.toList s'
ngramsState_migration :: NgramsState ngramsState_migration :: NgramsState
-> [(NodeId,NgramsState')] -> Map NodeId NgramsState'
ngramsState_migration ns = ngramsState_migration ns =
[ (nid, Map.singleton nt table) Map.fromListWith (Map.union) $
| (nt, nTable) <- Map.toList ns List.concat $
, (nid, table) <- Map.toList nTable map (\(nt, nTable)
] -> map (\(nid, table)
-> (nid, Map.singleton nt table)
) $ Map.toList nTable
) $ Map.toList ns
ngramsStatePatch_migration :: [NgramsStatePatch] ngramsStatePatch_migration :: [NgramsStatePatch]
-> Map NodeId [NgramsStatePatch'] -> Map NodeId [NgramsStatePatch']
ngramsStatePatch_migration np' = Map.fromListWith (<>) ngramsStatePatch_migration np' = Map.fromListWith (<>)
[ (nid, [fst $ Patch.singleton nt table]) $ List.concat
| np <- np' $ map toPatch np'
, (nt, nTable) <- Patch.toList np where
, (nid, table) <- Patch.toList nTable toPatch :: NgramsStatePatch -> [(NodeId, [NgramsStatePatch'])]
] toPatch p =
List.concat $
map (\(nt, nTable)
-> map (\(nid, table)
-> (nid, [fst $ Patch.singleton nt table])
) $ Patch.toList nTable
) $ Patch.toList p
------------------------------------------------------------------------ ------------------------------------------------------------------------
...@@ -230,7 +248,6 @@ instance (ToJSON s, ToJSON p) => ToJSON (Archive s p) where ...@@ -230,7 +248,6 @@ instance (ToJSON s, ToJSON p) => ToJSON (Archive s p) where
toEncoding = genericToEncoding $ unPrefix "_a_" toEncoding = genericToEncoding $ unPrefix "_a_"
------------------------------------------------------------------------ ------------------------------------------------------------------------
initNodeStory :: Monoid s => NodeId -> NodeStory s p initNodeStory :: Monoid s => NodeId -> NodeStory s p
initNodeStory ni = NodeStory $ Map.singleton ni initArchive initNodeStory ni = NodeStory $ Map.singleton ni initArchive
...@@ -240,7 +257,7 @@ initArchive = Archive 0 mempty [] ...@@ -240,7 +257,7 @@ initArchive = Archive 0 mempty []
initNodeListStoryMock :: NodeListStory initNodeListStoryMock :: NodeListStory
initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive
where where
nodeListId = 10 nodeListId = 0
archive = Archive 0 ngramsTableMap [] archive = Archive 0 ngramsTableMap []
ngramsTableMap = Map.singleton TableNgrams.NgramsTerms ngramsTableMap = Map.singleton TableNgrams.NgramsTerms
$ Map.fromList $ Map.fromList
...@@ -248,6 +265,9 @@ initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive ...@@ -248,6 +265,9 @@ initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive
| n <- mockTable ^. _NgramsTable | n <- mockTable ^. _NgramsTable
] ]
------------------------------------------------------------------------
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- | Lenses at the bottom of the file because Template Haskell would reorder order of execution in others cases -- | Lenses at the bottom of the file because Template Haskell would reorder order of execution in others cases
makeLenses ''NodeStoryEnv makeLenses ''NodeStoryEnv
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment