refactor registry to include OS version filtering on the package index (#64)

* refactor registry to include OS version filtering on the package index

* remove commented code, clean up tests

* removed unused types

* remove disabled test

* remove unused type

* fix query parsing

* remove unused code

* more purging

* MOAR PURGING

* normalize data model, fix all type errors

* rename get parameter
This commit is contained in:
Keagan McClelland
2021-10-26 14:53:36 -06:00
parent 39462a166a
commit bf9e3e313f
21 changed files with 304 additions and 686 deletions

View File

@@ -32,6 +32,7 @@ dependencies:
- filepath - filepath
- foreign-store - foreign-store
- fsnotify - fsnotify
- http-api-data
- http-types - http-types
- interpolate - interpolate
- lens - lens

View File

@@ -4,61 +4,109 @@
module Database.Marketplace where module Database.Marketplace where
import Conduit ( ConduitT
, MonadResource
, MonadUnliftIO
, awaitForever
, yield
)
import Database.Esqueleto.Experimental
( (%)
, (&&.)
, (++.)
, (==.)
, Entity(entityKey, entityVal)
, SqlBackend
, (^.)
, desc
, from
, ilike
, in_
, innerJoin
, on
, orderBy
, select
, selectSource
, val
, valList
, where_
, (||.)
)
import Database.Esqueleto.Experimental
( (:&)(..)
, table
)
import Lib.Types.AppIndex ( PkgId )
import Lib.Types.Category
import Lib.Types.Emver ( Version )
import Model
import Startlude hiding ( (%) import Startlude hiding ( (%)
, from , from
, on , on
, yield
) )
import Database.Esqueleto.Experimental
import Lib.Types.Category
import Model
import qualified Database.Persist as P
import Data.HashMap.Strict
import Data.Version
import Data.Aeson
searchServices :: MonadIO m => Maybe CategoryTitle -> Int64 -> Int64 -> Text -> ReaderT SqlBackend m [P.Entity SApp] searchServices :: (MonadResource m, MonadIO m)
searchServices Nothing pageItems offset' query = select $ do => Maybe CategoryTitle
service <- from $ table @SApp -> Text
-> ConduitT () (Entity PkgRecord) (ReaderT SqlBackend m) ()
searchServices Nothing query = selectSource $ do
service <- from $ table @PkgRecord
where_ where_
( (service ^. SAppDescShort `ilike` (%) ++. val query ++. (%)) ( (service ^. PkgRecordDescShort `ilike` (%) ++. val query ++. (%))
||. (service ^. SAppDescLong `ilike` (%) ++. val query ++. (%)) ||. (service ^. PkgRecordDescLong `ilike` (%) ++. val query ++. (%))
||. (service ^. SAppTitle `ilike` (%) ++. val query ++. (%)) ||. (service ^. PkgRecordTitle `ilike` (%) ++. val query ++. (%))
) )
orderBy [desc (service ^. SAppUpdatedAt)] orderBy [desc (service ^. PkgRecordUpdatedAt)]
limit pageItems
offset offset'
pure service pure service
searchServices (Just category) pageItems offset' query = select $ do searchServices (Just category) query = selectSource $ do
services <- from services <- from
(do (do
(service :& sc) <- (service :& _ :& cat) <-
from from
$ table @SApp $ table @PkgRecord
`innerJoin` table @ServiceCategory `innerJoin` table @PkgCategory
`on` (\(s :& sc) -> sc ^. ServiceCategoryServiceId ==. s ^. SAppId) `on` (\(s :& sc) -> sc ^. PkgCategoryPkgId ==. s ^. PkgRecordId)
-- if there is a cateogry, only search in category `innerJoin` table @Category
-- weight title, short, long (bitcoin should equal Bitcoin Core) `on` (\(_ :& sc :& cat) -> sc ^. PkgCategoryCategoryId ==. cat ^. CategoryId)
-- if there is a cateogry, only search in category
-- weight title, short, long (bitcoin should equal Bitcoin Core)
where_ where_
$ sc $ cat
^. ServiceCategoryCategoryName ^. CategoryName
==. val category ==. val category
&&. ( (service ^. SAppDescShort `ilike` (%) ++. val query ++. (%)) &&. ( (service ^. PkgRecordDescShort `ilike` (%) ++. val query ++. (%))
||. (service ^. SAppDescLong `ilike` (%) ++. val query ++. (%)) ||. (service ^. PkgRecordDescLong `ilike` (%) ++. val query ++. (%))
||. (service ^. SAppTitle `ilike` (%) ++. val query ++. (%)) ||. (service ^. PkgRecordTitle `ilike` (%) ++. val query ++. (%))
) )
pure service pure service
) )
orderBy [desc (services ^. SAppUpdatedAt)] orderBy [desc (services ^. PkgRecordUpdatedAt)]
limit pageItems
offset offset'
pure services pure services
newtype VersionsWithReleaseNotes = VersionsWithReleaseNotes (HashMap Version Text) deriving (Eq, Show, Generic) getPkgData :: (MonadResource m, MonadIO m) => [PkgId] -> ConduitT () (Entity PkgRecord) (ReaderT SqlBackend m) ()
instance FromJSON VersionsWithReleaseNotes getPkgData pkgs = selectSource $ do
instance PersistField VersionsWithReleaseNotes where pkgData <- from $ table @PkgRecord
fromPersistValue = fromPersistValueJSON where_ (pkgData ^. PkgRecordId `in_` valList (PkgRecordKey <$> pkgs))
toPersistValue = PersistText . show pure pkgData
-- in progress attempt to do postgres aggregation with raw sql in esqueleto zipVersions :: MonadUnliftIO m
-- getServiceVersionsWithReleaseNotes :: MonadIO m => Text -> ReaderT SqlBackend m (Entity SApp) => ConduitT (Entity PkgRecord) (Entity PkgRecord, [Entity VersionRecord]) (ReaderT SqlBackend m) ()
-- getServiceVersionsWithReleaseNotes appId = rawSql "SELECT ??, json_agg(json_build_object(v.number, v.release_notes)) as versions FROM s_app s LEFT JOIN version v ON v.app_id = s.id WHERE s.app_id = ? GROUP BY s.id;" [PersistText appId] zipVersions = awaitForever $ \i -> do
let appDbId = entityKey i
res <- lift $ select $ do
v <- from $ table @VersionRecord
where_ $ v ^. VersionRecordPkgId ==. val appDbId
pure v
yield (i, res)
filterOsCompatible :: Monad m
=> (Version -> Bool)
-> ConduitT
(Entity PkgRecord, [Entity VersionRecord])
(Entity PkgRecord, [Entity VersionRecord])
m
()
filterOsCompatible p = awaitForever $ \(app, versions) -> do
let compatible = filter (p . versionRecordOsVersion . entityVal) versions
when (not $ null compatible) $ yield (app, compatible)

View File

@@ -9,32 +9,15 @@ import Lib.Types.AppIndex
import Lib.Types.Emver import Lib.Types.Emver
import Model import Model
import Orphans.Emver ( ) import Orphans.Emver ( )
import Startlude import Startlude hiding ( get )
fetchApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe (Entity SApp)) fetchApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe PkgRecord)
fetchApp appId = selectFirst [SAppAppId ==. appId] [] fetchApp = get . PkgRecordKey
fetchAppVersion :: MonadIO m => Version -> Key SApp -> ReaderT SqlBackend m (Maybe (Entity SVersion)) fetchAppVersion :: MonadIO m => PkgId -> Version -> ReaderT SqlBackend m (Maybe VersionRecord)
fetchAppVersion appVersion appId = selectFirst [SVersionNumber ==. appVersion, SVersionAppId ==. appId] [] fetchAppVersion pkgId version = get (VersionRecordKey (PkgRecordKey pkgId) version)
createApp :: MonadIO m => PkgId -> StoreApp -> ReaderT SqlBackend m (Maybe (Key SApp)) createMetric :: MonadIO m => PkgId -> Version -> ReaderT SqlBackend m ()
createApp appId StoreApp {..} = do createMetric appId version = do
time <- liftIO getCurrentTime time <- liftIO getCurrentTime
insertUnique $ SApp time Nothing storeAppTitle appId storeAppDescShort storeAppDescLong storeAppIconType insert_ $ Metric time (PkgRecordKey appId) (VersionRecordKey (PkgRecordKey appId) version)
createAppVersion :: MonadIO m => Key SApp -> VersionInfo -> Text -> ReaderT SqlBackend m (Maybe (Key SVersion))
createAppVersion sId VersionInfo {..} arch = do
time <- liftIO getCurrentTime
insertUnique $ SVersion time
Nothing
sId
versionInfoVersion
versionInfoReleaseNotes
versionInfoOsRequired
versionInfoOsRecommended
(Just arch)
createMetric :: MonadIO m => Key SApp -> Key SVersion -> ReaderT SqlBackend m ()
createMetric appId versionId = do
time <- liftIO getCurrentTime
insert_ $ Metric time appId versionId

View File

@@ -6,6 +6,7 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Foundation where module Foundation where
import Startlude hiding ( Handler ) import Startlude hiding ( Handler )
@@ -64,12 +65,15 @@ instance Has PkgRepo RegistryCtx where
let repo = f $ extract ctx let repo = f $ extract ctx
settings = (appSettings ctx) { resourcesDir = pkgRepoFileRoot repo, staticBinDir = pkgRepoAppMgrBin repo } settings = (appSettings ctx) { resourcesDir = pkgRepoFileRoot repo, staticBinDir = pkgRepoAppMgrBin repo }
in ctx { appSettings = settings } in ctx { appSettings = settings }
instance Has PkgRepo (HandlerData RegistryCtx RegistryCtx) where instance Has a r => Has a (HandlerData r r) where
extract = extract . rheSite . handlerEnv extract = extract . rheSite . handlerEnv
update f r = update f r =
let ctx = update f (rheSite $ handlerEnv r) let ctx = update f (rheSite $ handlerEnv r)
rhe = (handlerEnv r) { rheSite = ctx, rheChild = ctx } rhe = (handlerEnv r) { rheSite = ctx, rheChild = ctx }
in r { handlerEnv = rhe } in r { handlerEnv = rhe }
instance Has AppSettings RegistryCtx where
extract = appSettings
update f ctx = ctx { appSettings = f (appSettings ctx) }

View File

@@ -11,16 +11,8 @@ module Handler.Apps where
import Startlude hiding ( Handler ) import Startlude hiding ( Handler )
import Control.Monad.Logger ( logError import Control.Monad.Logger ( logError )
, logInfo
)
import Data.Aeson ( ToJSON
, encode
)
import qualified Data.Attoparsec.Text as Atto
import qualified Data.ByteString.Lazy as BS
import qualified Data.Text as T import qualified Data.Text as T
import Database.Persist ( Entity(entityKey) )
import qualified GHC.Show ( Show(..) ) import qualified GHC.Show ( Show(..) )
import Network.HTTP.Types ( status404 ) import Network.HTTP.Types ( status404 )
import System.FilePath ( (<.>) import System.FilePath ( (<.>)
@@ -34,7 +26,6 @@ import Yesod.Core ( TypedContent
, sendResponseStatus , sendResponseStatus
, typeJson , typeJson
, typeOctet , typeOctet
, waiRequest
) )
import Yesod.Persist.Core ( YesodPersist(runDB) ) import Yesod.Persist.Core ( YesodPersist(runDB) )
@@ -55,37 +46,17 @@ import Lib.PkgRepository ( getBestVersion
) )
import Lib.Registry ( S9PK ) import Lib.Registry ( S9PK )
import Lib.Types.AppIndex ( PkgId(PkgId) ) import Lib.Types.AppIndex ( PkgId(PkgId) )
import Lib.Types.Emver ( Version import Lib.Types.Emver ( Version )
, parseVersion
)
import Network.Wai ( Request(requestHeaderUserAgent) )
import Util.Shared ( addPackageHeader import Util.Shared ( addPackageHeader
, getVersionSpecFromQuery , getVersionSpecFromQuery
, orThrow , orThrow
) )
pureLog :: Show a => a -> Handler a
pureLog = liftA2 (*>) ($logInfo . show) pure
logRet :: ToJSON a => Handler a -> Handler a
logRet = (>>= liftA2 (*>) ($logInfo . decodeUtf8 . BS.toStrict . encode) pure)
data FileExtension = FileExtension FilePath (Maybe String) data FileExtension = FileExtension FilePath (Maybe String)
instance Show FileExtension where instance Show FileExtension where
show (FileExtension f Nothing ) = f show (FileExtension f Nothing ) = f
show (FileExtension f (Just e)) = f <.> e show (FileExtension f (Just e)) = f <.> e
userAgentOsVersionParser :: Atto.Parser Version
userAgentOsVersionParser = do
void $ (Atto.string "EmbassyOS" <|> Atto.string "AmbassadorOS" <|> Atto.string "MeshOS") *> Atto.char '/'
parseVersion
getEmbassyOsVersion :: Handler (Maybe Version)
getEmbassyOsVersion = userAgentOsVersion
where
userAgentOsVersion =
(hush . Atto.parseOnly userAgentOsVersionParser . decodeUtf8 <=< requestHeaderUserAgent) <$> waiRequest
getAppManifestR :: PkgId -> Handler TypedContent getAppManifestR :: PkgId -> Handler TypedContent
getAppManifestR pkg = do getAppManifestR pkg = do
versionSpec <- getVersionSpecFromQuery versionSpec <- getVersionSpecFromQuery
@@ -116,12 +87,11 @@ recordMetrics pkg appVersion = do
Nothing -> do Nothing -> do
$logError $ [i|#{pkg} not found in database|] $logError $ [i|#{pkg} not found in database|]
notFound notFound
Just a -> do Just _ -> do
let appKey' = entityKey a existingVersion <- runDB $ fetchAppVersion pkg appVersion
existingVersion <- runDB $ fetchAppVersion appVersion appKey'
case existingVersion of case existingVersion of
Nothing -> do Nothing -> do
$logError $ [i|#{pkg}@#{appVersion} not found in database|] $logError $ [i|#{pkg}@#{appVersion} not found in database|]
notFound notFound
Just v -> runDB $ createMetric (entityKey a) (entityKey v) Just _ -> runDB $ createMetric pkg appVersion

View File

@@ -32,11 +32,6 @@ data IconType = PNG | JPG | JPEG | SVG
instance ToJSON IconType instance ToJSON IconType
instance FromJSON IconType instance FromJSON IconType
-- >>> readMaybe $ ixt :: Maybe IconType
-- Just PNG
ixt :: Text
ixt = toS $ toUpper <$> drop 1 ".png"
getIconsR :: PkgId -> Handler TypedContent getIconsR :: PkgId -> Handler TypedContent
getIconsR pkg = do getIconsR pkg = do
spec <- getVersionSpecFromQuery spec <- getVersionSpecFromQuery

View File

@@ -1,18 +1,16 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveAnyClass #-}
module Handler.Marketplace where module Handler.Marketplace where
import Startlude hiding ( Handler import Startlude hiding ( Any
, Handler
, ask
, from , from
, on , on
, sortOn , sortOn
@@ -20,10 +18,17 @@ import Startlude hiding ( Handler
import Conduit ( (.|) import Conduit ( (.|)
, awaitForever , awaitForever
, dropC
, mapC
, runConduit , runConduit
, sinkList
, sourceFile , sourceFile
, takeC
) )
import Control.Monad.Except.CoHas ( liftEither ) import Control.Monad.Except.CoHas ( liftEither )
import Control.Monad.Reader.Has ( Has
, ask
)
import Control.Parallel.Strategies ( parMap import Control.Parallel.Strategies ( parMap
, rpar , rpar
) )
@@ -51,32 +56,29 @@ import Data.String.Interpolate.IsString
( i ) ( i )
import qualified Data.Text as T import qualified Data.Text as T
import Database.Esqueleto.Experimental import Database.Esqueleto.Experimental
( (&&.) ( (:&)((:&))
, (:&)((:&))
, (==.) , (==.)
, (?.)
, Entity(entityKey, entityVal) , Entity(entityKey, entityVal)
, PersistEntity(Key)
, SqlBackend , SqlBackend
, Value(unValue) , Value(unValue)
, (^.) , (^.)
, desc , desc
, from , from
, groupBy , in_
, innerJoin , innerJoin
, just
, leftJoin
, limit
, on , on
, orderBy , orderBy
, select , select
, selectOne
, table , table
, val , val
, valList
, where_ , where_
) )
import Database.Esqueleto.PostgreSQL ( arrayAggDistinct ) import Database.Marketplace ( filterOsCompatible
import Database.Marketplace ( searchServices ) , getPkgData
, searchServices
, zipVersions
)
import qualified Database.Persist as P import qualified Database.Persist as P
import Foundation ( Handler import Foundation ( Handler
, RegistryCtx(appSettings) , RegistryCtx(appSettings)
@@ -89,19 +91,21 @@ import Lib.Types.AppIndex ( PkgId(PkgId)
, VersionInfo(..) , VersionInfo(..)
) )
import Lib.Types.AppIndex ( ) import Lib.Types.AppIndex ( )
import Lib.Types.Category ( CategoryTitle(FEATURED) ) import Lib.Types.Category ( CategoryTitle(..) )
import Lib.Types.Emver ( (<||) import Lib.Types.Emver ( (<||)
, Version , Version
, VersionRange , VersionRange(Any)
, parseRange
, parseVersion , parseVersion
, satisfies , satisfies
) )
import Model ( Category(..) import Model ( Category(..)
, EntityField(..) , EntityField(..)
, Key(PkgRecordKey, unPkgRecordKey)
, OsVersion(..) , OsVersion(..)
, SApp(..) , PkgCategory
, SVersion(..) , PkgRecord(..)
, ServiceCategory , VersionRecord(..)
) )
import Network.HTTP.Types ( status400 import Network.HTTP.Types ( status400
, status404 , status404
@@ -110,17 +114,10 @@ import Protolude.Unsafe ( unsafeFromJust )
import Settings ( AppSettings(registryHostname, resourcesDir) ) import Settings ( AppSettings(registryHostname, resourcesDir) )
import System.Directory ( getFileSize ) import System.Directory ( getFileSize )
import System.FilePath ( (</>) ) import System.FilePath ( (</>) )
import UnliftIO.Async ( concurrently import UnliftIO.Async ( mapConcurrently )
, mapConcurrently
)
import UnliftIO.Directory ( listDirectory ) import UnliftIO.Directory ( listDirectory )
import Util.Shared ( getVersionSpecFromQuery import Util.Shared ( getVersionSpecFromQuery )
, orThrow import Yesod.Core ( MonadResource
)
import Yesod.Core ( HandlerFor
, MonadLogger
, MonadResource
, MonadUnliftIO
, ToContent(..) , ToContent(..)
, ToTypedContent(..) , ToTypedContent(..)
, TypedContent , TypedContent
@@ -142,7 +139,6 @@ newtype CategoryRes = CategoryRes {
categories :: [CategoryTitle] categories :: [CategoryTitle]
} deriving (Show, Generic) } deriving (Show, Generic)
instance ToJSON CategoryRes instance ToJSON CategoryRes
instance FromJSON CategoryRes
instance ToContent CategoryRes where instance ToContent CategoryRes where
toContent = toContent . toJSON toContent = toContent . toJSON
instance ToTypedContent CategoryRes where instance ToTypedContent CategoryRes where
@@ -176,10 +172,6 @@ instance ToJSON ServiceRes where
, "versions" .= serviceResVersions , "versions" .= serviceResVersions
, "dependency-metadata" .= serviceResDependencyInfo , "dependency-metadata" .= serviceResDependencyInfo
] ]
instance ToContent ServiceRes where
toContent = toContent . toJSON
instance ToTypedContent ServiceRes where
toTypedContent = toTypedContent . toJSON
data DependencyInfo = DependencyInfo data DependencyInfo = DependencyInfo
{ dependencyInfoTitle :: PkgId { dependencyInfoTitle :: PkgId
, dependencyInfoIcon :: URL , dependencyInfoIcon :: URL
@@ -188,40 +180,6 @@ data DependencyInfo = DependencyInfo
instance ToJSON DependencyInfo where instance ToJSON DependencyInfo where
toJSON DependencyInfo {..} = object ["icon" .= dependencyInfoIcon, "title" .= dependencyInfoTitle] toJSON DependencyInfo {..} = object ["icon" .= dependencyInfoIcon, "title" .= dependencyInfoTitle]
data ServiceListRes = ServiceListRes
{ serviceListResCategories :: [CategoryTitle]
, serviceListResServices :: [ServiceAvailable]
}
deriving Show
instance ToJSON ServiceListRes where
toJSON ServiceListRes {..} =
object ["categories" .= serviceListResCategories, "services" .= serviceListResServices]
instance ToContent ServiceListRes where
toContent = toContent . toJSON
instance ToTypedContent ServiceListRes where
toTypedContent = toTypedContent . toJSON
data ServiceAvailable = ServiceAvailable
{ serviceAvailableId :: PkgId
, serviceAvailableTitle :: Text
, serviceAvailableVersion :: Version
, serviceAvailableIcon :: URL
, serviceAvailableDescShort :: Text
}
deriving Show
instance ToJSON ServiceAvailable where
toJSON ServiceAvailable {..} = object
[ "id" .= serviceAvailableId
, "title" .= serviceAvailableTitle
, "version" .= serviceAvailableVersion
, "icon" .= serviceAvailableIcon
, "descriptionShort" .= serviceAvailableDescShort
]
instance ToContent ServiceAvailable where
toContent = toContent . toJSON
instance ToTypedContent ServiceAvailable where
toTypedContent = toTypedContent . toJSON
newtype ServiceAvailableRes = ServiceAvailableRes [ServiceRes] newtype ServiceAvailableRes = ServiceAvailableRes [ServiceRes]
deriving (Generic) deriving (Generic)
instance ToJSON ServiceAvailableRes instance ToJSON ServiceAvailableRes
@@ -241,8 +199,8 @@ data OrderArrangement = ASC | DESC
deriving (Eq, Show, Read) deriving (Eq, Show, Read)
data ServiceListDefaults = ServiceListDefaults data ServiceListDefaults = ServiceListDefaults
{ serviceListOrder :: OrderArrangement { serviceListOrder :: OrderArrangement
, serviceListPageLimit :: Int64 -- the number of items per page , serviceListPageLimit :: Int -- the number of items per page
, serviceListPageNumber :: Int64 -- the page you are on , serviceListPageNumber :: Int -- the page you are on
, serviceListCategory :: Maybe CategoryTitle , serviceListCategory :: Maybe CategoryTitle
, serviceListQuery :: Text , serviceListQuery :: Text
} }
@@ -305,11 +263,8 @@ getReleaseNotesR = do
case lookup "id" getParameters of case lookup "id" getParameters of
Nothing -> sendResponseStatus status400 (InvalidParamsE "get:id" "<MISSING>") Nothing -> sendResponseStatus status400 (InvalidParamsE "get:id" "<MISSING>")
Just package -> do Just package -> do
(service, _) <- runDB $ fetchLatestApp (PkgId package) `orThrow` sendResponseStatus (_, notes) <- fetchAllAppVersions (PkgId package)
status404 pure notes
(NotFoundE $ show package)
(_, mappedVersions) <- fetchAllAppVersions (entityKey service)
pure mappedVersions
getEosR :: Handler TypedContent getEosR :: Handler TypedContent
getEosR = do getEosR = do
@@ -332,50 +287,70 @@ getVersionLatestR = do
case lookup "ids" getParameters of case lookup "ids" getParameters of
Nothing -> sendResponseStatus status400 (InvalidParamsE "get:ids" "<MISSING>") Nothing -> sendResponseStatus status400 (InvalidParamsE "get:ids" "<MISSING>")
Just packages -> case eitherDecode $ BS.fromStrict $ encodeUtf8 packages of Just packages -> case eitherDecode $ BS.fromStrict $ encodeUtf8 packages of
Left _ -> sendResponseStatus status400 (InvalidParamsE "get:ids" packages) Left _ -> sendResponseStatus status400 (InvalidParamsE "get:ids" packages)
Right (p :: [PkgId]) -> do Right p -> do
let packageList :: [(PkgId, Maybe Version)] = (, Nothing) <$> p let packageList = (, Nothing) <$> p
found <- runDB $ traverse fetchLatestApp $ fst <$> packageList found <- runDB $ traverse fetchLatestApp $ fst <$> packageList
pure pure
$ VersionLatestRes $ VersionLatestRes
$ HM.union $ HM.union
( HM.fromList ( HM.fromList
$ (\v -> (sAppAppId $ entityVal $ fst v, Just $ sVersionNumber $ entityVal $ snd v)) $ (\v ->
(unPkgRecordKey . entityKey $ fst v, Just $ versionRecordNumber $ entityVal $ snd v)
)
<$> catMaybes found <$> catMaybes found
) )
$ HM.fromList packageList $ HM.fromList packageList
getPackageListR :: Handler ServiceAvailableRes getPackageListR :: Handler ServiceAvailableRes
getPackageListR = do getPackageListR = do
pkgIds <- getPkgIdsQuery osPredicate <- getOsVersionQuery <&> \case
case pkgIds of Nothing -> const True
Just v -> flip satisfies v
pkgIds <- getPkgIdsQuery
filteredServices <- case pkgIds of
Nothing -> do Nothing -> do
-- query for all -- query for all
category <- getCategoryQuery category <- getCategoryQuery
page <- getPageQuery page <- getPageQuery
limit' <- getLimitQuery limit' <- getLimitQuery
query <- T.strip . fromMaybe (serviceListQuery defaults) <$> lookupGetParam "query" query <- T.strip . fromMaybe (serviceListQuery defaults) <$> lookupGetParam "query"
filteredServices <- runDB $ searchServices category limit' ((page - 1) * limit') query runDB
let filteredServices' = sAppAppId . entityVal <$> filteredServices $ runConduit
settings <- getsYesod appSettings $ searchServices category query
packageMetadata <- runDB $ fetchPackageMetadata .| zipVersions
serviceDetailResult <- mapConcurrently (getServiceDetails settings packageMetadata Nothing) .| filterOsCompatible osPredicate
filteredServices' -- pages start at 1 for some reason. TODO: make pages start at 0
let (_, services) = partitionEithers serviceDetailResult .| (dropC (limit' * (page - 1)) *> takeC limit')
pure $ ServiceAvailableRes services .| sinkList
Just packages -> do Just packages -> do
-- for each item in list get best available from version range -- for each item in list get best available from version range
settings <- getsYesod appSettings let vMap = (packageVersionId &&& packageVersionVersion) <$> packages
-- @TODO fix _ error runDB
packageMetadata <- runDB $ fetchPackageMetadata . runConduit
availableServicesResult <- traverse (getPackageDetails packageMetadata) packages $ getPkgData (packageVersionId <$> packages)
let (_, availableServices) = partitionEithers availableServicesResult .| zipVersions
serviceDetailResult <- mapConcurrently (uncurry $ getServiceDetails settings packageMetadata) .| mapC
availableServices (\(a, vs) ->
-- @TODO fix _ error let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) vMap
let (_, services) = partitionEithers serviceDetailResult in (a, filter ((<|| spec) . versionRecordNumber . entityVal) vs)
pure $ ServiceAvailableRes services )
.| filterOsCompatible osPredicate
.| sinkList
let keys = unPkgRecordKey . entityKey . fst <$> filteredServices
cats <- runDB $ fetchAppCategories keys
let vers =
filteredServices
<&> first (unPkgRecordKey . entityKey)
<&> second (sortOn Down . fmap (versionRecordNumber . entityVal))
& HM.fromListWith (++)
let packageMetadata = HM.intersectionWith (,) vers (categoryName <<$>> cats)
serviceDetailResult <- mapConcurrently (flip (getServiceDetails packageMetadata) Nothing)
(unPkgRecordKey . entityKey . fst <$> filteredServices)
let services = snd $ partitionEithers serviceDetailResult
pure $ ServiceAvailableRes services
where where
defaults = ServiceListDefaults { serviceListOrder = DESC defaults = ServiceListDefaults { serviceListOrder = DESC
, serviceListPageLimit = 20 , serviceListPageLimit = 20
@@ -401,7 +376,7 @@ getPackageListR = do
$logWarn (show e) $logWarn (show e)
sendResponseStatus status400 e sendResponseStatus status400 e
Just t -> pure $ Just t Just t -> pure $ Just t
getPageQuery :: Handler Int64 getPageQuery :: Handler Int
getPageQuery = lookupGetParam "page" >>= \case getPageQuery = lookupGetParam "page" >>= \case
Nothing -> pure $ serviceListPageNumber defaults Nothing -> pure $ serviceListPageNumber defaults
Just p -> case readMaybe p of Just p -> case readMaybe p of
@@ -412,7 +387,7 @@ getPackageListR = do
Just t -> pure $ case t of Just t -> pure $ case t of
0 -> 1 -- disallow page 0 so offset is not negative 0 -> 1 -- disallow page 0 so offset is not negative
_ -> t _ -> t
getLimitQuery :: Handler Int64 getLimitQuery :: Handler Int
getLimitQuery = lookupGetParam "per-page" >>= \case getLimitQuery = lookupGetParam "per-page" >>= \case
Nothing -> pure $ serviceListPageLimit defaults Nothing -> pure $ serviceListPageLimit defaults
Just pp -> case readMaybe pp of Just pp -> case readMaybe pp of
@@ -421,31 +396,23 @@ getPackageListR = do
$logWarn (show e) $logWarn (show e)
sendResponseStatus status400 e sendResponseStatus status400 e
Just l -> pure l Just l -> pure l
getPackageDetails :: MonadIO m getOsVersionQuery :: Handler (Maybe VersionRange)
=> (HM.HashMap PkgId ([Version], [CategoryTitle])) getOsVersionQuery = lookupGetParam "eos-version-range" >>= \case
-> PackageVersion Nothing -> pure Nothing
-> m (Either Text ((Maybe Version), PkgId)) Just osv -> case Atto.parseOnly parseRange osv of
getPackageDetails metadata pv = do Left _ -> do
let appId = packageVersionId pv let e = InvalidParamsE "get:eos-version-range" osv
let spec = packageVersionVersion pv $logWarn (show e)
pacakgeMetadata <- case HM.lookup appId metadata of sendResponseStatus status400 e
Nothing -> throwIO $ NotFoundE [i|dependency metadata for #{appId} not found.|] Right v -> pure $ Just v
Just m -> pure m
-- get best version from VersionRange of dependency
let satisfactory = filter (<|| spec) (fst pacakgeMetadata)
let best = getMax <$> foldMap (Just . Max) satisfactory
case best of
Nothing -> pure $ Left $ [i|Best version could not be found for #{appId} with spec #{spec}|]
Just v -> do
pure $ Right (Just v, appId)
getServiceDetails :: (MonadIO m, MonadResource m) getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, Has AppSettings r)
=> AppSettings => (HM.HashMap PkgId ([Version], [CategoryTitle]))
-> (HM.HashMap PkgId ([Version], [CategoryTitle]))
-> Maybe Version
-> PkgId -> PkgId
-> Maybe Version
-> m (Either S9Error ServiceRes) -> m (Either S9Error ServiceRes)
getServiceDetails settings metadata maybeVersion pkg = runExceptT $ do getServiceDetails metadata pkg maybeVersion = runExceptT $ do
settings <- ask
packageMetadata <- case HM.lookup pkg metadata of packageMetadata <- case HM.lookup pkg metadata of
Nothing -> liftEither . Left $ NotFoundE [i|#{pkg} not found.|] Nothing -> liftEither . Left $ NotFoundE [i|#{pkg} not found.|]
Just m -> pure m Just m -> pure m
@@ -494,117 +461,49 @@ mapDependencyMetadata domain metadata (appId, depInfo) = do
} }
) )
fetchAllAppVersions :: Key SApp -> HandlerFor RegistryCtx ([VersionInfo], ReleaseNotes) fetchAllAppVersions :: PkgId -> Handler ([VersionInfo], ReleaseNotes)
fetchAllAppVersions appId = do fetchAllAppVersions appId = do
entityAppVersions <- runDB $ P.selectList [SVersionAppId P.==. appId] [] entityAppVersions <- runDB $ P.selectList [VersionRecordPkgId P.==. PkgRecordKey appId] []
let vers = entityVal <$> entityAppVersions let vers = entityVal <$> entityAppVersions
let vv = mapSVersionToVersionInfo vers let vv = mapSVersionToVersionInfo vers
let mappedVersions = ReleaseNotes $ HM.fromList $ (\v -> (versionInfoVersion v, versionInfoReleaseNotes v)) <$> vv let mappedVersions = ReleaseNotes $ HM.fromList $ (\v -> (versionInfoVersion v, versionInfoReleaseNotes v)) <$> vv
pure (sortOn (Down . versionInfoVersion) vv, mappedVersions) pure (sortOn (Down . versionInfoVersion) vv, mappedVersions)
where where
mapSVersionToVersionInfo :: [SVersion] -> [VersionInfo] mapSVersionToVersionInfo :: [VersionRecord] -> [VersionInfo]
mapSVersionToVersionInfo sv = do mapSVersionToVersionInfo sv = do
(\v -> VersionInfo { versionInfoVersion = sVersionNumber v (\v -> VersionInfo { versionInfoVersion = versionRecordNumber v
, versionInfoReleaseNotes = sVersionReleaseNotes v , versionInfoReleaseNotes = versionRecordReleaseNotes v
, versionInfoDependencies = HM.empty , versionInfoDependencies = HM.empty
, versionInfoOsRequired = sVersionOsVersionRequired v , versionInfoOsVersion = versionRecordOsVersion v
, versionInfoOsRecommended = sVersionOsVersionRecommended v , versionInfoInstallAlert = Nothing
, versionInfoInstallAlert = Nothing
} }
) )
<$> sv <$> sv
fetchMostRecentAppVersions :: MonadIO m => Key SApp -> ReaderT SqlBackend m [Entity SVersion]
fetchMostRecentAppVersions appId = sortResults $ select $ do
version <- from $ table @SVersion
where_ (version ^. SVersionAppId ==. val appId)
limit 1
pure version
where sortResults = fmap $ sortOn (Down . sVersionNumber . entityVal)
fetchLatestApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe (P.Entity SApp, P.Entity SVersion)) fetchLatestApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe (P.Entity PkgRecord, P.Entity VersionRecord))
fetchLatestApp appId = fmap headMay . sortResults . select $ do fetchLatestApp appId = fmap headMay . sortResults . select $ do
(service :& version) <- (service :& version) <-
from from
$ table @SApp $ table @PkgRecord
`innerJoin` table @SVersion `innerJoin` table @VersionRecord
`on` (\(service :& version) -> service ^. SAppId ==. version ^. SVersionAppId) `on` (\(service :& version) -> service ^. PkgRecordId ==. version ^. VersionRecordPkgId)
where_ (service ^. SAppAppId ==. val appId) where_ (service ^. PkgRecordId ==. val (PkgRecordKey appId))
pure (service, version) pure (service, version)
where sortResults = fmap $ sortOn (Down . sVersionNumber . entityVal . snd) where sortResults = fmap $ sortOn (Down . versionRecordNumber . entityVal . snd)
fetchLatestAppAtVersion :: MonadIO m
=> PkgId
-> Version
-> ReaderT SqlBackend m (Maybe (P.Entity SApp, P.Entity SVersion))
fetchLatestAppAtVersion appId version' = selectOne $ do
(service :& version) <-
from
$ table @SApp
`innerJoin` table @SVersion
`on` (\(service :& version) -> service ^. SAppId ==. version ^. SVersionAppId)
where_ $ (service ^. SAppAppId ==. val appId) &&. (version ^. SVersionNumber ==. val version')
pure (service, version)
fetchPackageMetadata :: (MonadLogger m, MonadUnliftIO m)
=> ReaderT SqlBackend m (HM.HashMap PkgId ([Version], [CategoryTitle]))
fetchPackageMetadata = do
let categoriesQuery = select $ do
(service :& category) <-
from
$ table @SApp
`leftJoin` table @ServiceCategory
`on` (\(service :& category) ->
Database.Esqueleto.Experimental.just (service ^. SAppId)
==. category
?. ServiceCategoryServiceId
)
Database.Esqueleto.Experimental.groupBy $ service ^. SAppAppId
pure (service ^. SAppAppId, arrayAggDistinct (category ?. ServiceCategoryCategoryName))
let versionsQuery = select $ do
(service :& version) <-
from
$ table @SApp
`innerJoin` table @SVersion
`on` (\(service :& version) -> (service ^. SAppId) ==. version ^. SVersionAppId)
Database.Esqueleto.Experimental.groupBy $ (service ^. SAppAppId, version ^. SVersionNumber)
pure (service ^. SAppAppId, arrayAggDistinct (version ^. SVersionNumber))
(categories, versions) <- UnliftIO.Async.concurrently categoriesQuery versionsQuery
let
c = foreach categories
$ \(appId, categories') -> (unValue appId, catMaybes $ fromMaybe [] (unValue categories'))
let v = foreach versions $ \(appId, versions') -> (unValue appId, fromMaybe [] (unValue versions'))
let vv = HM.fromListWithKey (\_ vers vers' -> (++) vers vers') v
pure $ HM.intersectionWith (\cts vers -> (vers, cts)) (HM.fromList c) (sortVersions vv)
where sortVersions = fmap $ sortOn Down
fetchAppCategories :: MonadIO m => Key SApp -> ReaderT SqlBackend m [P.Entity ServiceCategory]
fetchAppCategories appId = select $ do
(categories :& service) <-
from
$ table @ServiceCategory
`innerJoin` table @SApp
`on` (\(sc :& s) -> sc ^. ServiceCategoryServiceId ==. s ^. SAppId)
where_ (service ^. SAppId ==. val appId)
pure categories
-- >>> encode hm
-- "{\"0.2.0\":\"some notes\"}"
hm :: Data.Aeson.Value
hm = object [ t .= v | (k, v) <- [("0.2.0", "some notes") :: (Version, Text)], let (String t) = toJSON k ]
-- >>> encode rn
-- "{\"0.2.0\":\"notes one\",\"0.3.0\":\"notes two\"}"
rn :: ReleaseNotes
rn = ReleaseNotes $ HM.fromList [("0.2.0", "notes one"), ("0.3.0", "notes two")]
-- >>> readMaybe $ cc :: Maybe CategoryTitle
-- Just FEATURED
cc :: Text
cc = T.toUpper "featured"
-- >>> encode ccc
-- "\"featured\""
ccc :: CategoryTitle
ccc = FEATURED
fetchAppCategories :: MonadIO m => [PkgId] -> ReaderT SqlBackend m (HM.HashMap PkgId [Category])
fetchAppCategories appIds = do
raw <- select $ do
(sc :& app :& cat) <-
from
$ table @PkgCategory
`innerJoin` table @PkgRecord
`on` (\(sc :& app) -> sc ^. PkgCategoryPkgId ==. app ^. PkgRecordId)
`innerJoin` table @Category
`on` (\(sc :& _ :& cat) -> sc ^. PkgCategoryCategoryId ==. cat ^. CategoryId)
where_ (sc ^. PkgCategoryPkgId `in_` valList (PkgRecordKey <$> appIds))
pure (app ^. PkgRecordId, cat)
let ls = fmap (first (unPkgRecordKey . unValue) . second (pure . entityVal)) raw
pure $ HM.fromListWith (++) ls

View File

@@ -8,7 +8,6 @@ import Startlude hiding ( toLower )
import Data.Aeson import Data.Aeson
import Yesod.Core.Content import Yesod.Core.Content
import Data.Text
import Lib.Types.Emver import Lib.Types.Emver
import Orphans.Emver ( ) import Orphans.Emver ( )
@@ -26,23 +25,3 @@ instance ToContent (Maybe AppVersionRes) where
toContent = toContent . toJSON toContent = toContent . toJSON
instance ToTypedContent (Maybe AppVersionRes) where instance ToTypedContent (Maybe AppVersionRes) where
toTypedContent = toTypedContent . toJSON toTypedContent = toTypedContent . toJSON
-- status - nothing, available, instuctions
-- version - semver string
data SystemStatus = NOTHING | AVAILABLE | INSTRUCTIONS
deriving (Eq, Show)
instance ToJSON SystemStatus where
toJSON = String . toLower . show
data OSVersionRes = OSVersionRes
{ osVersionStatus :: SystemStatus
, osVersionVersion :: Version
}
deriving (Eq, Show)
instance ToJSON OSVersionRes where
toJSON OSVersionRes {..} = object ["status" .= osVersionStatus, "version" .= osVersionVersion]
instance ToContent OSVersionRes where
toContent = toContent . toJSON
instance ToTypedContent OSVersionRes where
toTypedContent = toTypedContent . toJSON

View File

@@ -19,14 +19,10 @@ import Lib.Error ( S9Error(NotFoundE) )
import Lib.PkgRepository ( getBestVersion ) import Lib.PkgRepository ( getBestVersion )
import Lib.Types.AppIndex ( PkgId ) import Lib.Types.AppIndex ( PkgId )
import Network.HTTP.Types.Status ( status404 ) import Network.HTTP.Types.Status ( status404 )
import Settings
import Util.Shared ( getVersionSpecFromQuery import Util.Shared ( getVersionSpecFromQuery
, orThrow , orThrow
) )
getVersionR :: Handler AppVersionRes
getVersionR = AppVersionRes . registryVersion . appSettings <$> getYesod
getPkgVersionR :: PkgId -> Handler AppVersionRes getPkgVersionR :: PkgId -> Handler AppVersionRes
getPkgVersionR pkg = do getPkgVersionR pkg = do
spec <- getVersionSpecFromQuery spec <- getVersionSpecFromQuery

View File

@@ -64,19 +64,3 @@ toStatus = \case
NotFoundE _ -> status404 NotFoundE _ -> status404
InvalidParamsE _ _ -> status400 InvalidParamsE _ _ -> status400
AssetParseE _ _ -> status500 AssetParseE _ _ -> status500
handleS9ErrT :: MonadHandler m => S9ErrT m a -> m a
handleS9ErrT action = runExceptT action >>= \case
Left e -> toStatus >>= sendResponseStatus $ e
Right a -> pure a
handleS9ErrNuclear :: MonadIO m => S9ErrT m a -> m a
handleS9ErrNuclear action = runExceptT action >>= \case
Left e -> throwIO e
Right a -> pure a
errOnNothing :: MonadHandler m => Status -> Text -> Maybe a -> m a
errOnNothing status res entity = case entity of
Nothing -> sendResponseStatus status res
Just a -> pure a

View File

@@ -2,71 +2,18 @@
{-# LANGUAGE KindSignatures #-} {-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TemplateHaskell #-}
module Lib.Registry where module Lib.Registry where
import Startlude import Startlude
import qualified Data.Attoparsec.Text as Atto
import Data.HashMap.Lazy hiding ( mapMaybe )
import qualified GHC.Read ( Read(..) ) import qualified GHC.Read ( Read(..) )
import qualified GHC.Show ( Show(..) ) import qualified GHC.Show ( Show(..) )
import System.Directory
import System.FilePath import System.FilePath
import Yesod.Core import Yesod.Core
import Lib.Types.Emver
type Registry = HashMap String (HashMap Version FilePath)
newtype RegisteredAppVersion = RegisteredAppVersion { unRegisteredAppVersion :: (Version, FilePath) } deriving (Eq, Show)
data MaxVersion a = MaxVersion
{ getMaxVersion :: (a, a -> Version)
}
instance Semigroup (MaxVersion a) where
(MaxVersion (a, f)) <> (MaxVersion (b, g)) = if f a > g b then MaxVersion (a, f) else MaxVersion (b, g)
-- retrieve all valid semver folder names with queried for file: rootDirectory/appId/[0.0.0 ...]/appId.extension
-- @TODO move to db query after all appversions are seeded qith post 0.3.0 migration script
getAvailableAppVersions :: KnownSymbol a => FilePath -> Extension a -> IO [RegisteredAppVersion]
getAvailableAppVersions rootDirectory ext@(Extension appId) = do
versions <- mapMaybe (hush . Atto.parseOnly parseVersion . toS) <$> getSubDirectories (rootDirectory </> appId)
fmap catMaybes . for versions $ \v -> getVersionedFileFromDir rootDirectory ext v >>= \case
Nothing -> pure Nothing
Just appFile -> pure . Just $ RegisteredAppVersion (v, appFile)
where
getSubDirectories path = (fmap (fromRight []) . try @SomeException $ listDirectory path)
>>= filterM (doesDirectoryExist . (path </>))
-- this works for both service versions and embassyOS versions
getMostRecentAppVersion :: KnownSymbol a => FilePath -> Extension a -> IO (Maybe RegisteredAppVersion)
getMostRecentAppVersion rootDirectory ext = do
allVersions <- liftIO $ getAvailableAppVersions rootDirectory ext
pure $ head $ sortOn (Down . fst . unRegisteredAppVersion) allVersions
-- /root/appId/version/appId.ext
getVersionedFileFromDir :: KnownSymbol a => FilePath -> Extension a -> Version -> IO (Maybe FilePath)
getVersionedFileFromDir rootDirectory ext@(Extension appId) v =
getUnversionedFileFromDir (rootDirectory </> appId </> show v) ext
-- /root/appId.ext
getUnversionedFileFromDir :: KnownSymbol a => FilePath -> Extension a -> IO (Maybe FilePath)
getUnversionedFileFromDir rootDirectory appExt = fmap (join . hush) . try @SomeException $ do
dirContents <- listDirectory rootDirectory
pure . fmap (rootDirectory </>) $ find (== show appExt) dirContents
newtype Extension (a :: Symbol) = Extension String deriving (Eq) newtype Extension (a :: Symbol) = Extension String deriving (Eq)
type S9PK = Extension "s9pk" type S9PK = Extension "s9pk"
type SYS_EXTENSIONLESS = Extension ""
type PNG = Extension "png"
type SVG = Extension "svg"
instance IsString (Extension a) where
fromString = Extension
def :: Extension a
def = Extension ""
extension :: KnownSymbol a => Extension a -> String extension :: KnownSymbol a => Extension a -> String
extension = symbolVal extension = symbolVal
@@ -80,12 +27,6 @@ instance KnownSymbol a => Read (Extension a) where
other -> [ (Extension file, "") | ext' == "" <.> other ] other -> [ (Extension file, "") | ext' == "" <.> other ]
where (file, ext') = splitExtension s where (file, ext') = splitExtension s
withPeriod :: String -> String
withPeriod word@(a : _) = case a of
'.' -> word
_ -> "." <> word
withPeriod word = word
instance KnownSymbol a => PathPiece (Extension a) where instance KnownSymbol a => PathPiece (Extension a) where
fromPathPiece = readMaybe . toS fromPathPiece = readMaybe . toS
toPathPiece = show toPathPiece = show

View File

@@ -2,13 +2,13 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module Lib.Ssl where module Lib.Ssl where
import Startlude
import Data.String.Interpolate.IsString
import System.Directory import System.Directory
import System.Process import System.Process
import Data.String.Interpolate.IsString
import Startlude
import Foundation import Foundation
import Settings import Settings

View File

@@ -1,20 +0,0 @@
module Lib.SystemCtl where
import Startlude hiding ( words )
import Protolude.Unsafe
import Data.String
import System.Process
import Text.Casing
data ServiceAction =
StartService
| StopService
| RestartService
deriving (Eq, Show)
toAction :: ServiceAction -> String
toAction = fmap toLower . unsafeHead . words . wordify . show
systemCtl :: ServiceAction -> Text -> IO ExitCode
systemCtl action service = rawSystem "systemctl" [toAction action, toS service]

View File

@@ -1,38 +1,49 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
module Lib.Types.AppIndex where module Lib.Types.AppIndex where
import Startlude hiding ( Any ) import Startlude
import Control.Monad.Fail
import Data.Aeson
import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NE
import Control.Monad ( fail )
import Data.Aeson ( (.:)
, (.:?)
, FromJSON(..)
, FromJSONKey(..)
, ToJSON(..)
, ToJSONKey(..)
, withObject
)
import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy as BS
import Data.Functor.Contravariant ( Contravariant(contramap) ) import Data.Functor.Contravariant ( contramap )
import qualified Data.HashMap.Strict as HM
import Data.String.Interpolate.IsString import Data.String.Interpolate.IsString
-- import Model
import qualified Data.Text as T import qualified Data.Text as T
import Database.Persist.Postgresql import Database.Persist ( PersistField(..)
import qualified GHC.Read ( Read(..) ) , PersistValue(PersistText)
import qualified GHC.Show ( Show(..) ) , SqlType(..)
import Lib.Registry )
import Lib.Types.Emver import Database.Persist.Sql ( PersistFieldSql(sqlType) )
import GHC.Read ( Read(readsPrec) )
import Lib.Types.Emver ( Version
, VersionRange
)
import Orphans.Emver ( ) import Orphans.Emver ( )
import System.Directory import qualified Protolude.Base as P
import Yesod ( Show(..) )
import Web.HttpApiData ( FromHttpApiData
, ToHttpApiData
)
import Yesod ( PathPiece(..) )
newtype PkgId = PkgId { unPkgId :: Text } newtype PkgId = PkgId { unPkgId :: Text }
deriving (Eq) deriving stock (Eq, Ord)
deriving newtype (FromHttpApiData, ToHttpApiData)
instance IsString PkgId where instance IsString PkgId where
fromString = PkgId . fromString fromString = PkgId . fromString
instance Show PkgId where instance P.Show PkgId where
show = toS . unPkgId show = toS . unPkgId
instance Read PkgId where instance Read PkgId where
readsPrec _ s = [(PkgId $ toS s, "")] readsPrec _ s = [(PkgId $ toS s, "")]
@@ -55,101 +66,15 @@ instance PersistFieldSql PkgId where
instance PathPiece PkgId where instance PathPiece PkgId where
fromPathPiece = fmap PkgId . fromPathPiece fromPathPiece = fmap PkgId . fromPathPiece
toPathPiece = unPkgId toPathPiece = unPkgId
instance ToContent PkgId where
toContent = toContent . toJSON
instance ToTypedContent PkgId where
toTypedContent = toTypedContent . toJSON
data VersionInfo = VersionInfo data VersionInfo = VersionInfo
{ versionInfoVersion :: Version { versionInfoVersion :: Version
, versionInfoReleaseNotes :: Text , versionInfoReleaseNotes :: Text
, versionInfoDependencies :: HM.HashMap PkgId VersionRange , versionInfoDependencies :: HM.HashMap PkgId VersionRange
, versionInfoOsRequired :: VersionRange , versionInfoOsVersion :: Version
, versionInfoOsRecommended :: VersionRange , versionInfoInstallAlert :: Maybe Text
, versionInfoInstallAlert :: Maybe Text
} }
deriving (Eq, Show) deriving (Eq, Show)
instance Ord VersionInfo where
compare = compare `on` versionInfoVersion
instance FromJSON VersionInfo where
parseJSON = withObject "version info" $ \o -> do
versionInfoVersion <- o .: "version"
versionInfoReleaseNotes <- o .: "release-notes"
versionInfoDependencies <- o .:? "dependencies" .!= HM.empty
versionInfoOsRequired <- o .:? "os-version-required" .!= Any
versionInfoOsRecommended <- o .:? "os-version-recommended" .!= Any
versionInfoInstallAlert <- o .:? "install-alert"
pure VersionInfo { .. }
instance ToJSON VersionInfo where
toJSON VersionInfo {..} = object
[ "version" .= versionInfoVersion
, "release-notes" .= versionInfoReleaseNotes
, "dependencies" .= versionInfoDependencies
, "os-version-required" .= versionInfoOsRequired
, "os-version-recommended" .= versionInfoOsRecommended
, "install-alert" .= versionInfoInstallAlert
]
data StoreApp = StoreApp
{ storeAppTitle :: Text
, storeAppDescShort :: Text
, storeAppDescLong :: Text
, storeAppVersionInfo :: NonEmpty VersionInfo
, storeAppIconType :: Text
, storeAppTimestamp :: Maybe UTCTime
}
deriving Show
instance ToJSON StoreApp where
toJSON StoreApp {..} = object
[ "title" .= storeAppTitle
, "icon-type" .= storeAppIconType
, "description" .= object ["short" .= storeAppDescShort, "long" .= storeAppDescLong]
, "version-info" .= storeAppVersionInfo
, "timestamp" .= storeAppTimestamp
]
newtype AppManifest = AppManifest { unAppManifest :: HM.HashMap PkgId StoreApp}
deriving (Show)
instance FromJSON AppManifest where
parseJSON = withObject "app details to seed" $ \o -> do
apps <- for (HM.toList o) $ \(appId', c) -> do
appId <- parseJSON $ String appId'
config <- parseJSON c
storeAppTitle <- config .: "title"
storeAppIconType <- config .: "icon-type"
storeAppDescShort <- config .: "description" >>= (.: "short")
storeAppDescLong <- config .: "description" >>= (.: "long")
storeAppVersionInfo <- config .: "version-info" >>= \case
[] -> fail "No Valid Version Info"
(x : xs) -> pure $ x :| xs
storeAppTimestamp <- config .:? "timestamp"
pure (appId, StoreApp { .. })
return $ AppManifest (HM.fromList apps)
instance ToJSON AppManifest where
toJSON = toJSON . unAppManifest
filterOsRequired :: Version -> StoreApp -> Maybe StoreApp
filterOsRequired av sa = case NE.filter ((av <||) . versionInfoOsRequired) (storeAppVersionInfo sa) of
[] -> Nothing
(x : xs) -> Just $ sa { storeAppVersionInfo = x :| xs }
filterOsRecommended :: Version -> StoreApp -> Maybe StoreApp
filterOsRecommended av sa = case NE.filter ((av <||) . versionInfoOsRecommended) (storeAppVersionInfo sa) of
[] -> Nothing
(x : xs) -> Just $ sa { storeAppVersionInfo = x :| xs }
addFileTimestamp :: KnownSymbol a => FilePath -> Extension a -> StoreApp -> Version -> IO (Maybe StoreApp)
addFileTimestamp appDir ext service v = do
getVersionedFileFromDir appDir ext v >>= \case
Nothing -> pure Nothing
Just file -> do
time <- getModificationTime file
pure $ Just service { storeAppTimestamp = Just time }
data ServiceDependencyInfo = ServiceDependencyInfo data ServiceDependencyInfo = ServiceDependencyInfo
{ serviceDependencyInfoOptional :: Maybe Text { serviceDependencyInfoOptional :: Maybe Text
, serviceDependencyInfoVersion :: VersionRange , serviceDependencyInfoVersion :: VersionRange
@@ -164,27 +89,8 @@ instance FromJSON ServiceDependencyInfo where
serviceDependencyInfoDescription <- o .:? "description" serviceDependencyInfoDescription <- o .:? "description"
serviceDependencyInfoCritical <- o .: "critical" serviceDependencyInfoCritical <- o .: "critical"
pure ServiceDependencyInfo { .. } pure ServiceDependencyInfo { .. }
instance ToJSON ServiceDependencyInfo where
toJSON ServiceDependencyInfo {..} = object
[ "description" .= serviceDependencyInfoDescription
, "version" .= serviceDependencyInfoVersion
, "optional" .= serviceDependencyInfoOptional
, "critical" .= serviceDependencyInfoCritical
]
data ServiceAlert = INSTALL | UNINSTALL | RESTORE | START | STOP data ServiceAlert = INSTALL | UNINSTALL | RESTORE | START | STOP
deriving (Show, Eq, Generic, Hashable, Read) deriving (Show, Eq, Generic, Hashable, Read)
instance FromJSONKey ServiceAlert
instance ToJSONKey ServiceAlert
instance ToJSON ServiceAlert where
toJSON = String . T.toLower . show
instance FromJSON ServiceAlert where
parseJSON = withText "ServiceAlert" $ \case
"install" -> pure INSTALL
"uninstall" -> pure UNINSTALL
"restore" -> pure RESTORE
"start" -> pure START
"stop" -> pure STOP
_ -> fail "unknown service alert type"
data ServiceManifest = ServiceManifest data ServiceManifest = ServiceManifest
{ serviceManifestId :: !PkgId { serviceManifestId :: !PkgId
, serviceManifestTitle :: !Text , serviceManifestTitle :: !Text
@@ -216,16 +122,6 @@ instance FromJSON ServiceManifest where
let serviceManifestAlerts = HM.fromList a let serviceManifestAlerts = HM.fromList a
serviceManifestDependencies <- o .: "dependencies" serviceManifestDependencies <- o .: "dependencies"
pure ServiceManifest { .. } pure ServiceManifest { .. }
instance ToJSON ServiceManifest where
toJSON ServiceManifest {..} = object
[ "id" .= serviceManifestId
, "title" .= serviceManifestTitle
, "version" .= serviceManifestVersion
, "description" .= object ["short" .= serviceManifestDescriptionShort, "long" .= serviceManifestDescriptionLong]
, "release-notes" .= serviceManifestReleaseNotes
, "alerts" .= object [ t .= v | (k, v) <- HM.toList serviceManifestAlerts, let (String t) = toJSON k ]
, "dependencies" .= serviceManifestDependencies
]
-- >>> eitherDecode testManifest :: Either String ServiceManifest -- >>> eitherDecode testManifest :: Either String ServiceManifest
-- Right (ServiceManifest {serviceManifestId = embassy-pages, serviceManifestTitle = "Embassy Pages", serviceManifestVersion = 0.1.3, serviceManifestDescriptionLong = "Embassy Pages is a simple web server that uses directories inside File Browser to serve Tor websites.", serviceManifestDescriptionShort = "Create Tor websites, hosted on your Embassy.", serviceManifestReleaseNotes = "Upgrade to EmbassyOS v0.3.0", serviceManifestIcon = Just "icon.png", serviceManifestAlerts = fromList [(INSTALL,Nothing),(UNINSTALL,Nothing),(STOP,Nothing),(RESTORE,Nothing),(START,Nothing)], serviceManifestDependencies = fromList [(filebrowser,ServiceDependencyInfo {serviceDependencyInfoOptional = Nothing, serviceDependencyInfoVersion = >=2.14.1.1 <3.0.0, serviceDependencyInfoDescription = Just "Used to upload files to serve.", serviceDependencyInfoCritical = False})]}) -- Right (ServiceManifest {serviceManifestId = embassy-pages, serviceManifestTitle = "Embassy Pages", serviceManifestVersion = 0.1.3, serviceManifestDescriptionLong = "Embassy Pages is a simple web server that uses directories inside File Browser to serve Tor websites.", serviceManifestDescriptionShort = "Create Tor websites, hosted on your Embassy.", serviceManifestReleaseNotes = "Upgrade to EmbassyOS v0.3.0", serviceManifestIcon = Just "icon.png", serviceManifestAlerts = fromList [(INSTALL,Nothing),(UNINSTALL,Nothing),(STOP,Nothing),(RESTORE,Nothing),(START,Nothing)], serviceManifestDependencies = fromList [(filebrowser,ServiceDependencyInfo {serviceDependencyInfoOptional = Nothing, serviceDependencyInfoVersion = >=2.14.1.1 <3.0.0, serviceDependencyInfoDescription = Just "Used to upload files to serve.", serviceDependencyInfoCritical = False})]})

View File

@@ -1,6 +0,0 @@
module Lib.Types.FileSystem where
import Startlude
data FileExistence = Existent | NonExistent
deriving (Eq, Show)

View File

@@ -1,4 +1,6 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
@@ -6,7 +8,6 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DataKinds #-}
module Model where module Model where
@@ -18,28 +19,26 @@ import Orphans.Emver ( )
import Startlude import Startlude
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
SApp PkgRecord
Id PkgId sql=pkg_id
createdAt UTCTime createdAt UTCTime
updatedAt UTCTime Maybe updatedAt UTCTime Maybe
title Text title Text
appId PkgId
descShort Text descShort Text
descLong Text descLong Text
iconType Text iconType Text
UniqueAppId appId
deriving Eq deriving Eq
deriving Show deriving Show
SVersion sql=version VersionRecord sql=version
createdAt UTCTime createdAt UTCTime
updatedAt UTCTime Maybe updatedAt UTCTime Maybe
appId SAppId pkgId PkgRecordId
number Version number Version
releaseNotes Text releaseNotes Text
osVersionRequired VersionRange default='*' osVersion Version
osVersionRecommended VersionRange default='*'
arch Text Maybe arch Text Maybe
UniqueBin appId number Primary pkgId number
deriving Eq deriving Eq
deriving Show deriving Show
@@ -54,8 +53,8 @@ OsVersion
Metric Metric
createdAt UTCTime createdAt UTCTime
appId SAppId pkgId PkgRecordId
version SVersionId version VersionRecordId
deriving Eq deriving Eq
deriving Show deriving Show
@@ -69,13 +68,10 @@ Category
deriving Eq deriving Eq
deriving Show deriving Show
ServiceCategory PkgCategory
createdAt UTCTime createdAt UTCTime
serviceId SAppId pkgId PkgRecordId
categoryId CategoryId categoryId CategoryId
serviceName Text -- SAppAppId
categoryName CategoryTitle -- CategoryTitle
priority Int Maybe
deriving Eq deriving Eq
deriving Show deriving Show
|] |]

View File

@@ -1,13 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Orphans.Yesod where
import Startlude
import Yesod.Core
-- | Forgive me for I have sinned
instance ToJSON a => ToContent [a] where
toContent = toContent . toJSON . fmap toJSON
instance ToJSON a => ToTypedContent [a] where
toTypedContent = toTypedContent . toJSON . fmap toJSON

View File

@@ -1,12 +1,10 @@
module Startlude module Startlude
( module X ( module X
, module Startlude , module Startlude
) ) where
where
import Control.Arrow as X import Control.Arrow as X
( (&&&) ) ( (&&&) )
-- import Control.Comonad as X
import Control.Error.Util as X import Control.Error.Util as X
import Data.Coerce as X import Data.Coerce as X
import Data.String as X import Data.String as X
@@ -15,14 +13,14 @@ import Data.String as X
) )
import Data.Time.Clock as X import Data.Time.Clock as X
import Protolude as X import Protolude as X
hiding ( bool hiding ( (<.>)
, bool
, hush , hush
, isLeft , isLeft
, isRight , isRight
, note , note
, readMaybe , readMaybe
, tryIO , tryIO
, (<.>)
) )
import qualified Protolude as P import qualified Protolude as P
( readMaybe ) ( readMaybe )

View File

@@ -1,26 +0,0 @@
module Util.Function where
import Startlude
(.*) :: (b -> c) -> (a0 -> a1 -> b) -> a0 -> a1 -> c
(.*) = (.) . (.)
(.**) :: (b -> c) -> (a0 -> a1 -> a2 -> b) -> a0 -> a1 -> a2 -> c
(.**) = (.) . (.*)
preimage :: Eq b => (a -> b) -> b -> [a] -> [a]
preimage f target = filter ((== target) . f)
mapFind :: ([a] -> Maybe a) -> (b -> a) -> [b] -> Maybe b
mapFind _ _ [] = Nothing
mapFind finder mapping (b : bs) =
let mB = mapFind finder mapping bs
mA = finder [mapping b]
in case (mB, mA) of
(Just b', _ ) -> Just b'
(Nothing, Just _) -> Just b
_ -> Nothing
(<<&>>) :: (Functor f, Functor g) => f (g a) -> (a -> b) -> f (g b)
f <<&>> fab = fmap (fmap fab) f

View File

@@ -1,4 +1,3 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module Util.Shared where module Util.Shared where

View File

@@ -2,18 +2,21 @@
module Handler.MarketplaceSpec module Handler.MarketplaceSpec
( spec ( spec
) ) where
where
import Startlude hiding ( Any )
import Database.Persist.Sql
import Data.Maybe import Data.Maybe
import Database.Persist.Sql
import Startlude hiding ( Any )
import TestImport import Conduit ( (.|)
import Model , runConduit
, sinkList
)
import Database.Marketplace import Database.Marketplace
import Lib.Types.Category import Lib.Types.Category
import Lib.Types.Emver import Lib.Types.Emver
import Model
import TestImport
spec :: Spec spec :: Spec
spec = do spec = do
@@ -40,7 +43,7 @@ spec = do
_ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing _ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing _ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcon" BITCOIN Nothing _ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcon" BITCOIN Nothing
apps <- runDBtest $ searchServices (Just FEATURED) 20 0 "" apps <- runDBtest $ runConduit $ searchServices (Just FEATURED) "" .| sinkList
assertEq "should exist" (length apps) 1 assertEq "should exist" (length apps) 1
let app' = fromJust $ head apps let app' = fromJust $ head apps
assertEq "should be bitcoin" (sAppTitle $ entityVal app') "Bitcoin Core" assertEq "should be bitcoin" (sAppTitle $ entityVal app') "Bitcoin Core"
@@ -67,7 +70,7 @@ spec = do
_ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing _ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing _ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcoind" BITCOIN Nothing _ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcoind" BITCOIN Nothing
apps <- runDBtest $ searchServices (Just BITCOIN) 20 0 "" apps <- runDBtest $ runConduit $ searchServices (Just BITCOIN) "" .| sinkList
assertEq "should exist" (length apps) 2 assertEq "should exist" (length apps) 2
describe "searchServices with fuzzy query" describe "searchServices with fuzzy query"
$ withApp $ withApp
@@ -91,7 +94,7 @@ spec = do
cate <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 cate <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0
_ <- runDBtest $ insert_ $ ServiceCategory time app1 cate "bitcoind" FEATURED Nothing _ <- runDBtest $ insert_ $ ServiceCategory time app1 cate "bitcoind" FEATURED Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time app2 cate "lnd" FEATURED Nothing _ <- runDBtest $ insert_ $ ServiceCategory time app2 cate "lnd" FEATURED Nothing
apps <- runDBtest $ searchServices (Just FEATURED) 20 0 "lightning" apps <- runDBtest $ runConduit $ searchServices (Just FEATURED) "lightning" .| sinkList
assertEq "should exist" (length apps) 1 assertEq "should exist" (length apps) 1
let app' = fromJust $ head apps let app' = fromJust $ head apps
print app' print app'
@@ -123,14 +126,5 @@ spec = do
_ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing _ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing _ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing
_ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcon" BITCOIN Nothing _ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcon" BITCOIN Nothing
apps <- runDBtest $ searchServices Nothing 20 0 "" apps <- runDBtest $ runConduit $ searchServices Nothing "" .| sinkList
assertEq "should exist" (length apps) 2 assertEq "should exist" (length apps) 2
xdescribe "getServiceVersionsWithReleaseNotes"
$ withApp
$ it "gets service with mapping of version to release notes"
$ do
time <- liftIO getCurrentTime
app <- runDBtest $ insert $ SApp time Nothing "Bitcoin Core" "bitcoin" "short desc" "long desc" "png"
_ <- runDBtest $ insert $ SVersion time Nothing app "0.19.0.0" "release notes 0.19.0.0" Any Any Nothing
_ <- runDBtest $ insert $ SVersion time Nothing app "0.20.0.0" "release notes 0.19.0.0" Any Any Nothing
print ()