From 0cf13f46e696dcd700d33803d4ce2b397aef7af9 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Sun, 21 Nov 2021 14:19:08 -0700 Subject: [PATCH 01/12] fix handling of using index endpoint to fetch package at specific version --- src/Database/Marketplace.hs | 12 ++++--- src/Handler/Marketplace.hs | 72 ++++++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/Database/Marketplace.hs b/src/Database/Marketplace.hs index 98b924d..5463a29 100644 --- a/src/Database/Marketplace.hs +++ b/src/Database/Marketplace.hs @@ -38,7 +38,9 @@ import Database.Esqueleto.Experimental ) import Lib.Types.AppIndex ( PkgId ) import Lib.Types.Category -import Lib.Types.Emver ( Version ) +import Lib.Types.Emver ( Version + , VersionRange + ) import Model import Startlude hiding ( (%) , from @@ -103,10 +105,10 @@ zipVersions = awaitForever $ \i -> do filterOsCompatible :: Monad m => (Version -> Bool) -> ConduitT - (Entity PkgRecord, [Entity VersionRecord]) - (Entity PkgRecord, [Entity VersionRecord]) + (Entity PkgRecord, [Entity VersionRecord], VersionRange) + (Entity PkgRecord, [Entity VersionRecord], VersionRange) m () -filterOsCompatible p = awaitForever $ \(app, versions) -> do +filterOsCompatible p = awaitForever $ \(app, versions, requestedVersion) -> do let compatible = filter (p . versionRecordOsVersion . entityVal) versions - when (not $ null compatible) $ yield (app, compatible) + when (not $ null compatible) $ yield (app, compatible, requestedVersion) diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index b121323..6f8c38f 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -147,6 +147,10 @@ import Yesod.Core ( MonadResource ) import Yesod.Persist ( YesodDB ) import Yesod.Persist.Core ( YesodPersist(runDB) ) +import Data.Tuple.Extra hiding ( second + , first + , (&&&) + ) type URL = Text newtype CategoryRes = CategoryRes { @@ -166,8 +170,7 @@ data ServiceRes = ServiceRes , serviceResVersions :: [Version] , serviceResDependencyInfo :: HM.HashMap PkgId DependencyInfo } - deriving Generic - + deriving (Show, Generic) newtype ReleaseNotes = ReleaseNotes { unReleaseNotes :: HM.HashMap Version Text } deriving (Eq, Show) instance ToJSON ReleaseNotes where @@ -186,6 +189,16 @@ instance ToJSON ServiceRes where , "versions" .= serviceResVersions , "dependency-metadata" .= serviceResDependencyInfo ] +instance FromJSON ServiceRes where + parseJSON = withObject "ServiceRes" $ \o -> do + serviceResIcon <- o .: "icon" + serviceResLicense <- o .: "license" + serviceResInstructions <- o .: "instructions" + serviceResManifest <- o .: "manifest" + serviceResCategories <- o .: "categories" + serviceResVersions <- o .: "versions" + serviceResDependencyInfo <- o .: "dependency-metadata" + pure ServiceRes { .. } data DependencyInfo = DependencyInfo { dependencyInfoTitle :: PkgId , dependencyInfoIcon :: URL @@ -193,7 +206,11 @@ data DependencyInfo = DependencyInfo deriving (Eq, Show) instance ToJSON DependencyInfo where toJSON DependencyInfo {..} = object ["icon" .= dependencyInfoIcon, "title" .= dependencyInfoTitle] - +instance FromJSON DependencyInfo where + parseJSON = withObject "DependencyInfo" $ \o -> do + dependencyInfoIcon <- o .: "icon" + dependencyInfoTitle <- o .: "title" + pure DependencyInfo { .. } newtype ServiceAvailableRes = ServiceAvailableRes [ServiceRes] deriving (Generic) instance ToJSON ServiceAvailableRes @@ -346,39 +363,42 @@ getPackageListR = do $ runConduit $ searchServices category query .| zipVersions + .| mapC (\(a, vs) -> (,,) a vs Any) .| filterOsCompatible osPredicate -- pages start at 1 for some reason. TODO: make pages start at 0 .| (dropC (limit' * (page - 1)) *> takeC limit') .| sinkList - Just packages -> do + Just packages' -> do -- for each item in list get best available from version range - let vMap = (packageVersionId &&& packageVersionVersion) <$> packages + let vMap = (packageVersionId &&& packageVersionVersion) <$> packages' runDB . runConduit - $ getPkgData (packageVersionId <$> packages) + $ getPkgData (packageVersionId <$> packages') .| zipVersions .| mapC (\(a, vs) -> let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) vMap - in (a, filter ((<|| spec) . versionRecordNumber . entityVal) vs) + in (a, filter ((<|| spec) . versionRecordNumber . entityVal) vs, spec) ) .| filterOsCompatible osPredicate .| sinkList - let keys = unPkgRecordKey . entityKey . fst <$> filteredServices + let keys = unPkgRecordKey . entityKey . fst3 <$> filteredServices cats <- runDB $ fetchAppCategories keys let vers = filteredServices - <&> first (unPkgRecordKey . entityKey) - <&> second (sortOn Down . fmap (versionRecordNumber . entityVal)) - & HM.fromListWith (++) + <&> first3 (unPkgRecordKey . entityKey) + <&> second3 (sortOn Down . fmap (versionRecordNumber . entityVal)) + <&> (\(a, vs, vr) -> (,) a $ (,) vs vr) + & HM.fromListWith mergeDupes let packageMetadata = HM.intersectionWith (,) vers (categoryName <<$>> cats) - serviceDetailResult <- mapConcurrently (flip (getServiceDetails packageMetadata) Nothing) - (unPkgRecordKey . entityKey . fst <$> filteredServices) + serviceDetailResult <- mapConcurrently (getServiceDetails packageMetadata) + (unPkgRecordKey . entityKey . fst3 <$> filteredServices) let services = snd $ partitionEithers serviceDetailResult pure $ ServiceAvailableRes services - where + mergeDupes :: ([Version], VersionRange) -> ([Version], VersionRange) -> ([Version], VersionRange) + mergeDupes (vs, vr) (vs', _) = (,) ((++) vs vs') vr defaults = ServiceListDefaults { serviceListOrder = DESC , serviceListPageLimit = 20 , serviceListPageNumber = 1 @@ -434,23 +454,25 @@ getPackageListR = do Right v -> pure $ Just v getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, Has AppSettings r) - => (HM.HashMap PkgId ([Version], [CategoryTitle])) + => (HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) -> PkgId - -> Maybe Version -> m (Either S9Error ServiceRes) -getServiceDetails metadata pkg maybeVersion = runExceptT $ do +getServiceDetails metadata pkg = runExceptT $ do settings <- ask packageMetadata <- case HM.lookup pkg metadata of Nothing -> liftEither . Left $ NotFoundE [i|#{pkg} not found.|] Just m -> pure m - let domain = registryHostname settings - version <- case maybeVersion of - Nothing -> do + let domain = registryHostname settings + let versionInfo = fst $ (HM.!) metadata pkg + version <- case snd versionInfo of + Any -> do -- grab first value, which will be the latest version - case fst packageMetadata of + case fst versionInfo of [] -> liftEither . Left $ NotFoundE $ [i|No latest version found for #{pkg}|] x : _ -> pure x - Just v -> pure v + spec -> case headMay . sortOn Down $ filter (`satisfies` spec) $ fst versionInfo of + Nothing -> liftEither . Left $ NotFoundE [i|No version for #{pkg} satisfying #{spec}|] + Just v -> pure v manifest <- flip runReaderT settings $ (snd <$> getManifest pkg version) >>= \bs -> runConduit $ bs .| CL.foldMap BS.fromStrict case eitherDecode manifest of @@ -463,12 +485,12 @@ getServiceDetails metadata pkg maybeVersion = runExceptT $ do , serviceResCategories = snd packageMetadata , serviceResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] , serviceResLicense = [i|https://#{domain}/package/license/#{pkg}|] - , serviceResVersions = fst packageMetadata + , serviceResVersions = fst . fst $ packageMetadata , serviceResDependencyInfo = HM.fromList $ snd $ partitionEithers d } mapDependencyMetadata :: Text - -> HM.HashMap PkgId ([Version], [CategoryTitle]) + -> HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle]) -> (PkgId, ServiceDependencyInfo) -> Either S9Error (PkgId, DependencyInfo) mapDependencyMetadata domain metadata (appId, depInfo) = do @@ -476,7 +498,7 @@ mapDependencyMetadata domain metadata (appId, depInfo) = do Nothing -> Left $ NotFoundE [i|dependency metadata for #{appId} not found.|] Just m -> pure m -- get best version from VersionRange of dependency - let satisfactory = filter (<|| serviceDependencyInfoVersion depInfo) (fst depMetadata) + let satisfactory = filter (<|| serviceDependencyInfoVersion depInfo) (fst . fst $ depMetadata) let best = getMax <$> foldMap (Just . Max) satisfactory version <- case best of Nothing -> Left $ NotFoundE $ [i|No satisfactory version for dependent package #{appId}|] From 59fbdd4aa29b7854ce33e3361fa8abda1dacfdc5 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Sun, 21 Nov 2021 14:21:01 -0700 Subject: [PATCH 02/12] refactor test suite for model and api changes, adding tests for fetched versions via index endpoint --- test/Handler/AppSpec.hs | 134 ++++++++++++++++++++++---------- test/Handler/MarketplaceSpec.hs | 133 +++++++------------------------ test/Seed.hs | 76 ++++++++++++++++++ 3 files changed, 198 insertions(+), 145 deletions(-) create mode 100644 test/Seed.hs diff --git a/test/Handler/AppSpec.hs b/test/Handler/AppSpec.hs index 893afd2..ac619f0 100644 --- a/test/Handler/AppSpec.hs +++ b/test/Handler/AppSpec.hs @@ -1,4 +1,5 @@ {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ScopedTypeVariables #-} module Handler.AppSpec ( spec @@ -11,68 +12,119 @@ import Data.Maybe import TestImport import Model - +import Handler.Marketplace +import Seed +import Lib.Types.AppIndex +import Data.Aeson +import Data.Either.Extra spec :: Spec spec = do - describe "GET /package/index" $ withApp $ it "returns list of apps" $ do + describe "GET /package/index" $ withApp $ it "returns list of packages" $ do + _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/index" :: Text) - bodyContains "embassy-pages" - bodyContains "version: 0.1.3" statusIs 200 - describe "GET /package/:appId with unknown version spec for embassy-pages" + (res :: [ServiceRes]) <- requireJSONResponse + assertEq "response should have two packages" (length res) 3 + describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version" $ do + _ <- seedBitcoinLndStack + request $ do + setMethod "GET" + setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\"=0.21.1.2\"}]" :: Text) + statusIs 200 + (res :: [ServiceRes]) <- requireJSONResponse + assertEq "response should have one package" (length res) 1 + let pkg = fromJust $ head res + let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg + assertEq "manifest id should be bitcoind" (serviceManifestId manifest) "bitcoind" + xdescribe "GET /package/index?ids" $ withApp - $ it "fails to get unknown app" + $ it "returns list of packages and dependencies at specified version" $ do + _ <- seedBitcoinLndStack request $ do setMethod "GET" - setUrl ("/package/embassy-pages.s9pk?spec=0.1.4" :: Text) - statusIs 404 - describe "GET /package/:appId with unknown app" $ withApp $ it "fails to get an unregistered app" $ do + setUrl ("/package/index?ids=[{\"id\":\"lnd\",\"version\":\"=0.13.3.1\"}]" :: Text) + statusIs 200 + (res :: [ServiceRes]) <- requireJSONResponse + assertEq "response should have one package" (length res) 1 + let pkg = fromJust $ head res + printBody + assertEq "package dependency metadata should not be empty" (null $ serviceResDependencyInfo pkg) False + describe "GET /package/index?ids" $ withApp $ it "returns list of packages at exactly specified version" $ do + _ <- seedBitcoinLndStack + request $ do + setMethod "GET" + setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\"=0.21.1.1\"}]" :: Text) + statusIs 200 + (res :: [ServiceRes]) <- requireJSONResponse + assertEq "response should have one package" (length res) 1 + let pkg = fromJust $ head res + let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg + assertEq "manifest version should be 0.21.1.1" (serviceManifestVersion manifest) "0.21.1.1" + describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version or greater" $ do + _ <- seedBitcoinLndStack + request $ do + setMethod "GET" + setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\">=0.21.1.1\"}]" :: Text) + statusIs 200 + (res :: [ServiceRes]) <- requireJSONResponse + assertEq "response should have one package" (length res) 1 + let pkg = fromJust $ head res + let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg + assertEq "manifest version should be 0.21.1.2" (serviceManifestVersion manifest) "0.21.1.2" + describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version or greater" $ do + _ <- seedBitcoinLndStack + request $ do + setMethod "GET" + setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\">=0.21.1.2\"}]" :: Text) + statusIs 200 + (res :: [ServiceRes]) <- requireJSONResponse + assertEq "response should have one package" (length res) 1 + let pkg = fromJust $ head res + let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg + assertEq "manifest version should be 0.21.1.2" (serviceManifestVersion manifest) "0.21.1.2" + describe "GET /package/:pkgId with unknown version spec for bitcoind" $ withApp $ it "fails to get unknown app" $ do + _ <- seedBitcoinLndStack + request $ do + setMethod "GET" + setUrl ("/package/bitcoind.s9pk?spec==0.20.0" :: Text) + statusIs 404 + xdescribe "GET /package/:pkgId with unknown package" $ withApp $ it "fails to get an unregistered app" $ do + _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/tempapp.s9pk?spec=0.0.1" :: Text) statusIs 404 - describe "GET /package/:appId with existing version spec for embassy-pages" + xdescribe "GET /package/:pkgId with package at unknown version" + $ withApp + $ it "fails to get an unregistered app" + $ do + _ <- seedBitcoinLndStack + request $ do + setMethod "GET" + setUrl ("/package/lightning.s9pk?spec==0.0.1" :: Text) + statusIs 404 + describe "GET /package/:pkgId with existing version spec for bitcoind" $ withApp $ it "creates app and metric records" $ do + _ <- seedBitcoinLndStack request $ do setMethod "GET" - setUrl ("/package/embassy-pages.s9pk?spec==0.1.3" :: Text) + setUrl ("/package/bitcoind.s9pk?spec==0.21.1.2" :: Text) statusIs 200 - apps <- runDBtest $ selectList [SAppAppId ==. "embassy-pages"] [] - assertEq "app should exist" (length apps) 1 - let app = fromJust $ head apps - metrics <- runDBtest $ selectList [MetricAppId ==. entityKey app] [] + packages <- runDBtest $ selectList [PkgRecordId ==. PkgRecordKey "bitcoind"] [] + assertEq "app should exist" (length packages) 1 + let app = fromJust $ head packages + metrics <- runDBtest $ selectList [MetricPkgId ==. entityKey app] [] assertEq "metric should exist" (length metrics) 1 - describe "GET /package/:appId with existing version spec for filebrowser" - $ withApp - $ it "creates app and metric records" - $ do - request $ do - setMethod "GET" - setUrl ("/package/filebrowser.s9pk?spec==2.14.1.1" :: Text) - statusIs 200 - apps <- runDBtest $ selectList [SAppAppId ==. "filebrowser"] [] - assertEq "app should exist" (length apps) 1 - let app = fromJust $ head apps - metrics <- runDBtest $ selectList [MetricAppId ==. entityKey app] [] - assertEq "metric should exist" (length metrics) 1 - version <- runDBtest $ selectList [SVersionAppId ==. entityKey app] [] - assertEq "version should exist" (length version) 1 - describe "GET /sys/proxy.pac" $ withApp $ it "does not record metric but request successful" $ do + describe "GET /package/:pkgId with existing version spec for lnd" $ withApp $ it "creates metric records" $ do + _ <- seedBitcoinLndStack request $ do setMethod "GET" - setUrl ("/sys/proxy.pac?spec=0.1.0" :: Text) + setUrl ("/package/lnd.s9pk?spec=>=0.13.3.0" :: Text) statusIs 200 - apps <- runDBtest $ selectList ([] :: [Filter SApp]) [] - assertEq "no apps should exist" (length apps) 0 - describe "GET /sys/:sysId" $ withApp $ it "does not record metric but request successful" $ do - request $ do - setMethod "GET" - setUrl ("/sys/appmgr?spec=0.0.0" :: Text) - statusIs 200 - apps <- runDBtest $ selectList ([] :: [Filter SApp]) [] - assertEq "no apps should exist" (length apps) 0 + metrics <- runDBtest $ selectList [MetricPkgId ==. PkgRecordKey "lnd"] [] + assertEq "metric should exist" (length metrics) 1 diff --git a/test/Handler/MarketplaceSpec.hs b/test/Handler/MarketplaceSpec.hs index 2430fcc..f6f6e99 100644 --- a/test/Handler/MarketplaceSpec.hs +++ b/test/Handler/MarketplaceSpec.hs @@ -2,7 +2,8 @@ module Handler.MarketplaceSpec ( spec - ) where + ) +where import Data.Maybe import Database.Persist.Sql @@ -14,117 +15,41 @@ import Conduit ( (.|) ) import Database.Marketplace import Lib.Types.Category -import Lib.Types.Emver import Model import TestImport +import Seed spec :: Spec spec = do describe "searchServices with category" $ withApp $ it "should filter services with featured category" $ do - time <- liftIO getCurrentTime - btc <- runDBtest $ insert $ SApp time - (Just time) - "Bitcoin Core" - "bitcoind" - "short desc bitcoin" - "long desc bitcoin" - "png" - lnd <- runDBtest $ insert $ SApp time - (Just time) - "Lightning Network Daemon" - "lnd" - "short desc lnd" - "long desc lnd" - "png" - featuredCat <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 - btcCat <- runDBtest $ insert $ Category time BITCOIN Nothing "desc" 0 - lnCat <- runDBtest $ insert $ Category time LIGHTNING Nothing "desc" 0 - _ <- runDBtest $ insert_ $ ServiceCategory time btc featuredCat "bitcoin" FEATURED Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcon" BITCOIN Nothing - apps <- runDBtest $ runConduit $ searchServices (Just FEATURED) "" .| sinkList - assertEq "should exist" (length apps) 1 - let app' = fromJust $ head apps - assertEq "should be bitcoin" (sAppTitle $ entityVal app') "Bitcoin Core" + _ <- seedBitcoinLndStack + packages <- runDBtest $ runConduit $ searchServices (Just FEATURED) "" .| sinkList + assertEq "should exist" (length packages) 1 + let pkg = fromJust $ head packages + assertEq "should be bitcoin" (pkgRecordTitle $ entityVal pkg) "Bitcoin Core" describe "searchServices with category" $ withApp $ it "should filter services with bitcoin category" $ do - time <- liftIO getCurrentTime - btc <- runDBtest $ insert $ SApp time - (Just time) - "Bitcoin Core" - "bitcoind" - "short desc bitcoin" - "long desc bitcoin" - "png" - lnd <- runDBtest $ insert $ SApp time - (Just time) - "Lightning Network Daemon" - "lnd" - "short desc lnd" - "long desc lnd" - "png" - featuredCat <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 - btcCat <- runDBtest $ insert $ Category time BITCOIN Nothing "desc" 0 - lnCat <- runDBtest $ insert $ Category time LIGHTNING Nothing "desc" 0 - _ <- runDBtest $ insert_ $ ServiceCategory time btc featuredCat "bitcoind" FEATURED Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcoind" BITCOIN Nothing - apps <- runDBtest $ runConduit $ searchServices (Just BITCOIN) "" .| sinkList - assertEq "should exist" (length apps) 2 + _ <- seedBitcoinLndStack + packages <- runDBtest $ runConduit $ searchServices (Just BITCOIN) "" .| sinkList + assertEq "should exist" (length packages) 3 describe "searchServices with fuzzy query" $ withApp - $ it "runs search service with fuzzy text in long description" + $ it "runs search service with fuzzy text in long description and no category" $ do - time <- liftIO getCurrentTime - app1 <- runDBtest $ insert $ SApp time - (Just time) - "Bitcoin Core" - "bitcoind" - "short desc" - "long desc" - "png" - app2 <- runDBtest $ insert $ SApp time - (Just time) - "Lightning Network Daemon" - "lnd" - "short desc" - "lightning long desc" - "png" - cate <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 - _ <- runDBtest $ insert_ $ ServiceCategory time app1 cate "bitcoind" FEATURED Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time app2 cate "lnd" FEATURED Nothing - apps <- runDBtest $ runConduit $ searchServices (Just FEATURED) "lightning" .| sinkList - assertEq "should exist" (length apps) 1 - let app' = fromJust $ head apps - print app' + _ <- seedBitcoinLndStack + packages <- runDBtest $ runConduit $ searchServices Nothing "lightning" .| sinkList + assertEq "should exist" (length packages) 1 + let pkg = fromJust $ head packages + print pkg + describe "searchServices with fuzzy query" + $ withApp + $ it "runs search service with fuzzy text in long description and bitcoin category" + $ do + _ <- seedBitcoinLndStack + packages <- runDBtest $ runConduit $ searchServices (Just BITCOIN) "proxy" .| sinkList + assertEq "should exist" (length packages) 1 + let pkg = fromJust $ head packages + print pkg describe "searchServices with any category" $ withApp $ it "runs search service for any category" $ do - time <- liftIO getCurrentTime - btc <- runDBtest $ insert $ SApp time - (Just time) - "Bitcoin Core" - "bitcoind" - "short desc bitcoin" - "long desc bitcoin" - "png" - print btc - _ <- runDBtest $ insert $ SVersion time (Just time) btc "0.19.0" "notes" Any Any Nothing - _ <- runDBtest $ insert $ SVersion time (Just time) btc "0.20.0" "notes" Any Any Nothing - lnd <- runDBtest $ insert $ SApp time - (Just time) - "Lightning Network Daemon" - "lnd" - "short desc lnd" - "long desc lnd" - "png" - _ <- runDBtest $ insert $ SVersion time (Just time) lnd "0.18.0" "notes" Any Any Nothing - _ <- runDBtest $ insert $ SVersion time (Just time) lnd "0.17.0" "notes" Any Any Nothing - featuredCat <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 - btcCat <- runDBtest $ insert $ Category time BITCOIN Nothing "desc" 0 - lnCat <- runDBtest $ insert $ Category time LIGHTNING Nothing "desc" 0 - _ <- runDBtest $ insert_ $ ServiceCategory time btc featuredCat "bitcoin" FEATURED Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time lnd lnCat "lnd" LIGHTNING Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time lnd btcCat "lnd" BITCOIN Nothing - _ <- runDBtest $ insert_ $ ServiceCategory time btc btcCat "bitcon" BITCOIN Nothing - apps <- runDBtest $ runConduit $ searchServices Nothing "" .| sinkList - assertEq "should exist" (length apps) 2 + _ <- seedBitcoinLndStack + packages <- runDBtest $ runConduit $ searchServices Nothing "" .| sinkList + assertEq "should exist" (length packages) 3 diff --git a/test/Seed.hs b/test/Seed.hs new file mode 100644 index 0000000..ee831f9 --- /dev/null +++ b/test/Seed.hs @@ -0,0 +1,76 @@ +module Seed where + +import Startlude ( ($) + , Applicative(pure) + , Maybe(Nothing, Just) + , getCurrentTime + , MonadIO(liftIO) + ) +import Database.Persist.Sql ( PersistStoreWrite(insert_, insertKey, insert) ) +import Model ( Key(PkgRecordKey) + , PkgRecord(PkgRecord) + , Category(Category) + , PkgCategory(PkgCategory) + , VersionRecord(VersionRecord) + ) + +import TestImport ( runDBtest + , RegistryCtx + , SIO + , YesodExampleData + ) +import Lib.Types.Category ( CategoryTitle(LIGHTNING, FEATURED, BITCOIN) ) + +seedBitcoinLndStack :: SIO (YesodExampleData RegistryCtx) () +seedBitcoinLndStack = do + time <- liftIO getCurrentTime + _ <- runDBtest $ insertKey (PkgRecordKey "bitcoind") $ PkgRecord time + (Just time) + "Bitcoin Core" + "short desc bitcoin" + "long desc bitcoin" + "png" + _ <- runDBtest $ insert $ VersionRecord time + (Just time) + (PkgRecordKey "bitcoind") + "0.21.1.2" + "notes" + "0.3.0" + Nothing + _ <- runDBtest $ insert $ VersionRecord time + (Just time) + (PkgRecordKey "bitcoind") + "0.21.1.1" + "notes" + "0.3.0" + Nothing + _ <- runDBtest $ insertKey (PkgRecordKey "lnd") $ PkgRecord time + (Just time) + "Lightning Network Daemon" + "short desc lnd" + "long desc lnd" + "png" + _ <- runDBtest $ insert $ VersionRecord time (Just time) (PkgRecordKey "lnd") "0.13.3.0" "notes" "0.3.0" Nothing + _ <- runDBtest $ insert $ VersionRecord time (Just time) (PkgRecordKey "lnd") "0.13.3.1" "notes" "0.3.0" Nothing + _ <- runDBtest $ insertKey (PkgRecordKey "btc-rpc-proxy") $ PkgRecord time + (Just time) + "BTC RPC Proxy" + "short desc btc-rpc-proxy" + "long desc btc-rpc-proxy" + "png" + _ <- runDBtest $ insert $ VersionRecord time + (Just time) + (PkgRecordKey "btc-rpc-proxy") + "0.3.2.1" + "notes" + "0.3.0" + Nothing + featuredCat <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 + btcCat <- runDBtest $ insert $ Category time BITCOIN Nothing "desc" 0 + lnCat <- runDBtest $ insert $ Category time LIGHTNING Nothing "desc" 0 + _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "bitcoind") featuredCat + _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "lnd") lnCat + _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "lnd") btcCat + _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "bitcoind") btcCat + _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "btc-rpc-proxy") btcCat + pure () From 8180e8feb9516f4a3606ea59ce44258f05b7da5b Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Sun, 21 Nov 2021 17:18:20 -0700 Subject: [PATCH 03/12] handle errors in either cases --- src/Handler/Marketplace.hs | 44 +++++++++++++++++++++++--------------- src/Lib/Error.hs | 6 ++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index 6f8c38f..9e74ee0 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -92,7 +92,9 @@ import Database.Persist ( PersistUniqueRead(getBy) import Foundation ( Handler , RegistryCtx(appSettings) ) -import Lib.Error ( S9Error(..) ) +import Lib.Error ( S9Error(..) + , toStatus + ) import Lib.PkgRepository ( getManifest ) import Lib.Types.AppIndex ( PkgId(PkgId) , ServiceDependencyInfo(serviceDependencyInfoVersion) @@ -138,7 +140,6 @@ import Yesod.Core ( MonadResource , addHeader , getRequest , getsYesod - , logWarn , lookupGetParam , respondSource , sendChunkBS @@ -151,6 +152,7 @@ import Data.Tuple.Extra hiding ( second , first , (&&&) ) +import Control.Monad.Logger type URL = Text newtype CategoryRes = CategoryRes { @@ -393,8 +395,11 @@ getPackageListR = do let packageMetadata = HM.intersectionWith (,) vers (categoryName <<$>> cats) serviceDetailResult <- mapConcurrently (getServiceDetails packageMetadata) (unPkgRecordKey . entityKey . fst3 <$> filteredServices) - let services = snd $ partitionEithers serviceDetailResult - pure $ ServiceAvailableRes services + let res = partitionEithers serviceDetailResult + case fst res of + -- just throw first error? + x : _ -> sendResponseStatus (toStatus x) x + [] -> pure $ ServiceAvailableRes $ snd res where mergeDupes :: ([Version], VersionRange) -> ([Version], VersionRange) -> ([Version], VersionRange) @@ -453,7 +458,7 @@ getPackageListR = do sendResponseStatus status400 e Right v -> pure $ Just v -getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, Has AppSettings r) +getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, MonadLogger m, Has AppSettings r) => (HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) -> PkgId -> m (Either S9Error ServiceRes) @@ -478,30 +483,35 @@ getServiceDetails metadata pkg = runExceptT $ do case eitherDecode manifest of Left _ -> liftEither . Left $ AssetParseE [i|#{pkg}:manifest|] (decodeUtf8 $ BS.toStrict manifest) Right m -> do - let d = parMap rpar (mapDependencyMetadata domain metadata) (HM.toList $ serviceManifestDependencies m) - pure $ ServiceRes { serviceResIcon = [i|https://#{domain}/package/icon/#{pkg}|] + let deps = partitionEithers $ parMap rpar + (mapDependencyMetadata domain metadata) + (HM.toList $ serviceManifestDependencies m) + case fst deps of + _ : xs -> do + liftEither . Left $ DepMetadataE xs + [] -> pure $ ServiceRes { serviceResIcon = [i|https://#{domain}/package/icon/#{pkg}|] -- pass through raw JSON Value, we have checked its correct parsing above - , serviceResManifest = unsafeFromJust . decode $ manifest - , serviceResCategories = snd packageMetadata - , serviceResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] - , serviceResLicense = [i|https://#{domain}/package/license/#{pkg}|] - , serviceResVersions = fst . fst $ packageMetadata - , serviceResDependencyInfo = HM.fromList $ snd $ partitionEithers d - } + , serviceResManifest = unsafeFromJust . decode $ manifest + , serviceResCategories = snd packageMetadata + , serviceResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] + , serviceResLicense = [i|https://#{domain}/package/license/#{pkg}|] + , serviceResVersions = fst . fst $ packageMetadata + , serviceResDependencyInfo = HM.fromList $ snd deps + } mapDependencyMetadata :: Text -> HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle]) -> (PkgId, ServiceDependencyInfo) - -> Either S9Error (PkgId, DependencyInfo) + -> Either Text (PkgId, DependencyInfo) mapDependencyMetadata domain metadata (appId, depInfo) = do depMetadata <- case HM.lookup appId metadata of - Nothing -> Left $ NotFoundE [i|dependency metadata for #{appId} not found.|] + Nothing -> Left [i|dependency metadata for #{appId} not found.|] Just m -> pure m -- get best version from VersionRange of dependency let satisfactory = filter (<|| serviceDependencyInfoVersion depInfo) (fst . fst $ depMetadata) let best = getMax <$> foldMap (Just . Max) satisfactory version <- case best of - Nothing -> Left $ NotFoundE $ [i|No satisfactory version for dependent package #{appId}|] + Nothing -> Left [i|No satisfactory version for dependent package #{appId}|] Just v -> pure v pure ( appId diff --git a/src/Lib/Error.hs b/src/Lib/Error.hs index 5c9e9a6..acc85bf 100644 --- a/src/Lib/Error.hs +++ b/src/Lib/Error.hs @@ -8,6 +8,7 @@ import Startlude import Data.String.Interpolate.IsString import Network.HTTP.Types import Yesod.Core +import qualified Data.Text as T type S9ErrT m = ExceptT S9Error m @@ -17,6 +18,7 @@ data S9Error = | NotFoundE Text | InvalidParamsE Text Text | AssetParseE Text Text + | DepMetadataE [Text] deriving (Show, Eq) instance Exception S9Error @@ -29,6 +31,9 @@ toError = \case NotFoundE e -> Error NOT_FOUND [i|#{e}|] InvalidParamsE e m -> Error INVALID_PARAMS [i|Could not parse request parameters #{e}: #{m}|] AssetParseE asset found -> Error PARSE_ERROR [i|Could not parse #{asset}: #{found}|] + DepMetadataE errs -> do + let errorText = T.concat errs + Error NOT_FOUND [i|#{errorText}|] data ErrorCode = DATABASE_ERROR @@ -64,3 +69,4 @@ toStatus = \case NotFoundE _ -> status404 InvalidParamsE _ _ -> status400 AssetParseE _ _ -> status500 + DepMetadataE _ -> status404 From 1b6f21094fe19df9e890d016c4069ec1231c1399 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:05:41 -0700 Subject: [PATCH 04/12] always get package dependencies when querying for a specified package --- src/Handler/Marketplace.hs | 103 ++++++++++++++++++++++---------- src/Lib/Types/AppIndex.hs | 1 + test/Handler/AppSpec.hs | 3 +- test/Handler/MarketplaceSpec.hs | 4 +- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index 9e74ee0..0c5c54b 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -25,11 +25,10 @@ import Conduit ( (.|) , sinkList , sourceFile , takeC + , MonadUnliftIO ) import Control.Monad.Except.CoHas ( liftEither ) -import Control.Monad.Reader.Has ( Has - , ask - ) + import Control.Parallel.Strategies ( parMap , rpar ) @@ -90,7 +89,7 @@ import Database.Persist ( PersistUniqueRead(getBy) , insertUnique ) import Foundation ( Handler - , RegistryCtx(appSettings) + , RegistryCtx(appSettings, appConnPool) ) import Lib.Error ( S9Error(..) , toStatus @@ -145,6 +144,7 @@ import Yesod.Core ( MonadResource , sendChunkBS , sendResponseStatus , typeOctet + , getYesod ) import Yesod.Persist ( YesodDB ) import Yesod.Persist.Core ( YesodPersist(runDB) ) @@ -153,6 +153,11 @@ import Data.Tuple.Extra hiding ( second , (&&&) ) import Control.Monad.Logger +import Database.Persist.Sql ( runSqlPool ) +import Database.Persist.Postgresql ( ConnectionPool ) +import Control.Monad.Reader.Has ( Has + , ask + ) type URL = Text newtype CategoryRes = CategoryRes { @@ -354,7 +359,7 @@ getPackageListR = do Nothing -> const True Just v -> flip satisfies v pkgIds <- getPkgIdsQuery - filteredServices <- case pkgIds of + filteredPackages <- case pkgIds of Nothing -> do -- query for all category <- getCategoryQuery @@ -384,26 +389,18 @@ getPackageListR = do ) .| filterOsCompatible osPredicate .| sinkList - let keys = unPkgRecordKey . entityKey . fst3 <$> filteredServices - cats <- runDB $ fetchAppCategories keys - let vers = - filteredServices - <&> first3 (unPkgRecordKey . entityKey) - <&> second3 (sortOn Down . fmap (versionRecordNumber . entityVal)) - <&> (\(a, vs, vr) -> (,) a $ (,) vs vr) - & HM.fromListWith mergeDupes - let packageMetadata = HM.intersectionWith (,) vers (categoryName <<$>> cats) - serviceDetailResult <- mapConcurrently (getServiceDetails packageMetadata) - (unPkgRecordKey . entityKey . fst3 <$> filteredServices) - let res = partitionEithers serviceDetailResult - case fst res of - -- just throw first error? - x : _ -> sendResponseStatus (toStatus x) x - [] -> pure $ ServiceAvailableRes $ snd res + (keys, packageMetadata) <- runDB $ createPackageMetadata filteredPackages + appConnPool <- appConnPool <$> getYesod + serviceDetailResult <- mapConcurrently (getServiceDetails osPredicate appConnPool packageMetadata) keys + let (errors, res) = partitionEithers serviceDetailResult + case errors of + x : xs -> do + -- log all errors but just throw first error until Validation implemented - TODO https://hackage.haskell.org/package/validation + for_ xs (\e -> $logWarn [i|Get package list errors: #{e}|]) + sendResponseStatus (toStatus x) x + [] -> pure $ ServiceAvailableRes res where - mergeDupes :: ([Version], VersionRange) -> ([Version], VersionRange) -> ([Version], VersionRange) - mergeDupes (vs, vr) (vs', _) = (,) ((++) vs vs') vr defaults = ServiceListDefaults { serviceListOrder = DESC , serviceListPageLimit = 20 , serviceListPageNumber = 1 @@ -458,11 +455,33 @@ getPackageListR = do sendResponseStatus status400 e Right v -> pure $ Just v -getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, MonadLogger m, Has AppSettings r) - => (HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) +mergeDupes :: ([Version], VersionRange) -> ([Version], VersionRange) -> ([Version], VersionRange) +mergeDupes (vs, vr) (vs', _) = (,) ((++) vs vs') vr + +createPackageMetadata :: (MonadReader r m, MonadIO m) + => [(Entity PkgRecord, [Entity VersionRecord], VersionRange)] + -> ReaderT + SqlBackend + m + ([PkgId], HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) +createPackageMetadata pkgs = do + let keys = unPkgRecordKey . entityKey . fst3 <$> pkgs + cats <- fetchAppCategories keys + let vers = + pkgs + <&> first3 (unPkgRecordKey . entityKey) + <&> second3 (sortOn Down . fmap (versionRecordNumber . entityVal)) + <&> (\(a, vs, vr) -> (,) a $ (,) vs vr) + & HM.fromListWith mergeDupes + pure $ (keys, HM.intersectionWith (,) vers (categoryName <<$>> cats)) + +getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, MonadLogger m, Has AppSettings r, MonadUnliftIO m) + => (Version -> Bool) + -> ConnectionPool + -> (HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) -> PkgId -> m (Either S9Error ServiceRes) -getServiceDetails metadata pkg = runExceptT $ do +getServiceDetails osPredicate appConnPool metadata pkg = runExceptT $ do settings <- ask packageMetadata <- case HM.lookup pkg metadata of Nothing -> liftEither . Left $ NotFoundE [i|#{pkg} not found.|] @@ -483,12 +502,15 @@ getServiceDetails metadata pkg = runExceptT $ do case eitherDecode manifest of Left _ -> liftEither . Left $ AssetParseE [i|#{pkg}:manifest|] (decodeUtf8 $ BS.toStrict manifest) Right m -> do - let deps = partitionEithers $ parMap rpar - (mapDependencyMetadata domain metadata) - (HM.toList $ serviceManifestDependencies m) - case fst deps of - _ : xs -> do - liftEither . Left $ DepMetadataE xs + let depVerList = + (fst &&& (serviceDependencyInfoVersion . snd)) <$> (HM.toList $ serviceManifestDependencies m) + (_, depMetadata) <- lift $ runSqlPool (createPackageMetadata =<< getDependencies depVerList) appConnPool + let (errors, deps) = partitionEithers $ parMap + rpar + (mapDependencyMetadata domain $ (HM.union depMetadata metadata)) + (HM.toList $ serviceManifestDependencies m) + case errors of + _ : xs -> liftEither . Left $ DepMetadataE xs [] -> pure $ ServiceRes { serviceResIcon = [i|https://#{domain}/package/icon/#{pkg}|] -- pass through raw JSON Value, we have checked its correct parsing above , serviceResManifest = unsafeFromJust . decode $ manifest @@ -496,8 +518,23 @@ getServiceDetails metadata pkg = runExceptT $ do , serviceResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] , serviceResLicense = [i|https://#{domain}/package/license/#{pkg}|] , serviceResVersions = fst . fst $ packageMetadata - , serviceResDependencyInfo = HM.fromList $ snd deps + , serviceResDependencyInfo = HM.fromList deps } + where + getDependencies :: (MonadResource m, MonadUnliftIO m) + => [(PkgId, VersionRange)] + -> ReaderT SqlBackend m [(Entity PkgRecord, [Entity VersionRecord], VersionRange)] + getDependencies deps = + runConduit + $ getPkgData (fst <$> deps) + .| zipVersions + .| mapC + (\(a, vs) -> + let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) deps + in (a, filter ((<|| spec) . versionRecordNumber . entityVal) vs, spec) + ) + .| filterOsCompatible osPredicate + .| sinkList mapDependencyMetadata :: Text -> HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle]) diff --git a/src/Lib/Types/AppIndex.hs b/src/Lib/Types/AppIndex.hs index 8ebda17..7ffdd62 100644 --- a/src/Lib/Types/AppIndex.hs +++ b/src/Lib/Types/AppIndex.hs @@ -75,6 +75,7 @@ data VersionInfo = VersionInfo } deriving (Eq, Show) +-- TODO rename to PackageDependencyInfo data ServiceDependencyInfo = ServiceDependencyInfo { serviceDependencyInfoOptional :: Maybe Text , serviceDependencyInfoVersion :: VersionRange diff --git a/test/Handler/AppSpec.hs b/test/Handler/AppSpec.hs index ac619f0..1f7c4e5 100644 --- a/test/Handler/AppSpec.hs +++ b/test/Handler/AppSpec.hs @@ -38,7 +38,7 @@ spec = do let pkg = fromJust $ head res let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg assertEq "manifest id should be bitcoind" (serviceManifestId manifest) "bitcoind" - xdescribe "GET /package/index?ids" + describe "GET /package/index?ids" $ withApp $ it "returns list of packages and dependencies at specified version" $ do @@ -50,7 +50,6 @@ spec = do (res :: [ServiceRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - printBody assertEq "package dependency metadata should not be empty" (null $ serviceResDependencyInfo pkg) False describe "GET /package/index?ids" $ withApp $ it "returns list of packages at exactly specified version" $ do _ <- seedBitcoinLndStack diff --git a/test/Handler/MarketplaceSpec.hs b/test/Handler/MarketplaceSpec.hs index f6f6e99..6d83bc1 100644 --- a/test/Handler/MarketplaceSpec.hs +++ b/test/Handler/MarketplaceSpec.hs @@ -39,7 +39,7 @@ spec = do packages <- runDBtest $ runConduit $ searchServices Nothing "lightning" .| sinkList assertEq "should exist" (length packages) 1 let pkg = fromJust $ head packages - print pkg + assertEq "package should be lnd" (entityKey pkg) (PkgRecordKey "lnd") describe "searchServices with fuzzy query" $ withApp $ it "runs search service with fuzzy text in long description and bitcoin category" @@ -48,7 +48,7 @@ spec = do packages <- runDBtest $ runConduit $ searchServices (Just BITCOIN) "proxy" .| sinkList assertEq "should exist" (length packages) 1 let pkg = fromJust $ head packages - print pkg + assertEq "package should be lnc" (entityKey pkg) (PkgRecordKey "btc-rpc-proxy") describe "searchServices with any category" $ withApp $ it "runs search service for any category" $ do _ <- seedBitcoinLndStack packages <- runDBtest $ runConduit $ searchServices Nothing "" .| sinkList From eacaa0c78fa340d176541b9bf310bcb7dd79a47b Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Mon, 22 Nov 2021 13:06:05 -0700 Subject: [PATCH 05/12] rename all references from services to packages --- src/Handler/Marketplace.hs | 180 +++++++++++++++++++------------------ src/Lib/PkgRepository.hs | 4 +- src/Lib/Types/AppIndex.hs | 70 ++++++++------- test/Handler/AppSpec.hs | 32 +++---- 4 files changed, 148 insertions(+), 138 deletions(-) diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index 0c5c54b..0da92bc 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -96,8 +96,8 @@ import Lib.Error ( S9Error(..) ) import Lib.PkgRepository ( getManifest ) import Lib.Types.AppIndex ( PkgId(PkgId) - , ServiceDependencyInfo(serviceDependencyInfoVersion) - , ServiceManifest(serviceManifestDependencies) + , PackageDependency(packageDependencyVersion) + , PackageManifest(packageManifestDependencies) , VersionInfo(..) ) import Lib.Types.AppIndex ( ) @@ -168,14 +168,14 @@ instance ToContent CategoryRes where toContent = toContent . toJSON instance ToTypedContent CategoryRes where toTypedContent = toTypedContent . toJSON -data ServiceRes = ServiceRes - { serviceResIcon :: URL - , serviceResManifest :: Data.Aeson.Value -- ServiceManifest - , serviceResCategories :: [CategoryTitle] - , serviceResInstructions :: URL - , serviceResLicense :: URL - , serviceResVersions :: [Version] - , serviceResDependencyInfo :: HM.HashMap PkgId DependencyInfo +data PackageRes = PackageRes + { packageResIcon :: URL + , packageResManifest :: Data.Aeson.Value -- PackageManifest + , packageResCategories :: [CategoryTitle] + , packageResInstructions :: URL + , packageResLicense :: URL + , packageResVersions :: [Version] + , packageResDependencies :: HM.HashMap PkgId DependencyRes } deriving (Show, Generic) newtype ReleaseNotes = ReleaseNotes { unReleaseNotes :: HM.HashMap Version Text } @@ -186,44 +186,44 @@ instance ToContent ReleaseNotes where toContent = toContent . toJSON instance ToTypedContent ReleaseNotes where toTypedContent = toTypedContent . toJSON -instance ToJSON ServiceRes where - toJSON ServiceRes {..} = object - [ "icon" .= serviceResIcon - , "license" .= serviceResLicense - , "instructions" .= serviceResInstructions - , "manifest" .= serviceResManifest - , "categories" .= serviceResCategories - , "versions" .= serviceResVersions - , "dependency-metadata" .= serviceResDependencyInfo +instance ToJSON PackageRes where + toJSON PackageRes {..} = object + [ "icon" .= packageResIcon + , "license" .= packageResLicense + , "instructions" .= packageResInstructions + , "manifest" .= packageResManifest + , "categories" .= packageResCategories + , "versions" .= packageResVersions + , "dependency-metadata" .= packageResDependencies ] -instance FromJSON ServiceRes where - parseJSON = withObject "ServiceRes" $ \o -> do - serviceResIcon <- o .: "icon" - serviceResLicense <- o .: "license" - serviceResInstructions <- o .: "instructions" - serviceResManifest <- o .: "manifest" - serviceResCategories <- o .: "categories" - serviceResVersions <- o .: "versions" - serviceResDependencyInfo <- o .: "dependency-metadata" - pure ServiceRes { .. } -data DependencyInfo = DependencyInfo - { dependencyInfoTitle :: PkgId - , dependencyInfoIcon :: URL +instance FromJSON PackageRes where + parseJSON = withObject "PackageRes" $ \o -> do + packageResIcon <- o .: "icon" + packageResLicense <- o .: "license" + packageResInstructions <- o .: "instructions" + packageResManifest <- o .: "manifest" + packageResCategories <- o .: "categories" + packageResVersions <- o .: "versions" + packageResDependencies <- o .: "dependency-metadata" + pure PackageRes { .. } +data DependencyRes = DependencyRes + { dependencyResTitle :: PkgId + , dependencyResIcon :: URL } deriving (Eq, Show) -instance ToJSON DependencyInfo where - toJSON DependencyInfo {..} = object ["icon" .= dependencyInfoIcon, "title" .= dependencyInfoTitle] -instance FromJSON DependencyInfo where - parseJSON = withObject "DependencyInfo" $ \o -> do - dependencyInfoIcon <- o .: "icon" - dependencyInfoTitle <- o .: "title" - pure DependencyInfo { .. } -newtype ServiceAvailableRes = ServiceAvailableRes [ServiceRes] +instance ToJSON DependencyRes where + toJSON DependencyRes {..} = object ["icon" .= dependencyResIcon, "title" .= dependencyResTitle] +instance FromJSON DependencyRes where + parseJSON = withObject "DependencyRes" $ \o -> do + dependencyResIcon <- o .: "icon" + dependencyResTitle <- o .: "title" + pure DependencyRes { .. } +newtype PackageListRes = PackageListRes [PackageRes] deriving (Generic) -instance ToJSON ServiceAvailableRes -instance ToContent ServiceAvailableRes where +instance ToJSON PackageListRes +instance ToContent PackageListRes where toContent = toContent . toJSON -instance ToTypedContent ServiceAvailableRes where +instance ToTypedContent PackageListRes where toTypedContent = toTypedContent . toJSON newtype VersionLatestRes = VersionLatestRes (HM.HashMap PkgId (Maybe Version)) @@ -235,12 +235,12 @@ instance ToTypedContent VersionLatestRes where toTypedContent = toTypedContent . toJSON data OrderArrangement = ASC | DESC deriving (Eq, Show, Read) -data ServiceListDefaults = ServiceListDefaults - { serviceListOrder :: OrderArrangement - , serviceListPageLimit :: Int -- the number of items per page - , serviceListPageNumber :: Int -- the page you are on - , serviceListCategory :: Maybe CategoryTitle - , serviceListQuery :: Text +data PackageListDefaults = PackageListDefaults + { packageListOrder :: OrderArrangement + , packageListPageLimit :: Int -- the number of items per page + , packageListPageNumber :: Int -- the page you are on + , packageListCategory :: Maybe CategoryTitle + , packageListQuery :: Text } deriving (Eq, Show, Read) data EosRes = EosRes @@ -257,16 +257,16 @@ instance ToContent EosRes where instance ToTypedContent EosRes where toTypedContent = toTypedContent . toJSON -data PackageVersion = PackageVersion - { packageVersionId :: PkgId - , packageVersionVersion :: VersionRange +data PackageReq = PackageReq + { packageReqId :: PkgId + , packageReqVersion :: VersionRange } deriving Show -instance FromJSON PackageVersion where +instance FromJSON PackageReq where parseJSON = withObject "package version" $ \o -> do - packageVersionId <- o .: "id" - packageVersionVersion <- o .: "version" - pure PackageVersion { .. } + packageReqId <- o .: "id" + packageReqVersion <- o .: "version" + pure PackageReq { .. } getCategoriesR :: Handler CategoryRes getCategoriesR = do @@ -353,19 +353,26 @@ getVersionLatestR = do ) $ HM.fromList packageList -getPackageListR :: Handler ServiceAvailableRes +getPackageListR :: Handler PackageListRes getPackageListR = do osPredicate <- getOsVersionQuery <&> \case Nothing -> const True Just v -> flip satisfies v pkgIds <- getPkgIdsQuery + -- deep info + -- generate data from db + -- filter os + -- filter from request + -- shallow info - generate get deps + -- transformations + -- assemble api response filteredPackages <- case pkgIds of Nothing -> do -- query for all category <- getCategoryQuery page <- getPageQuery limit' <- getLimitQuery - query <- T.strip . fromMaybe (serviceListQuery defaults) <$> lookupGetParam "query" + query <- T.strip . fromMaybe (packageListQuery defaults) <$> lookupGetParam "query" runDB $ runConduit $ searchServices category query @@ -377,10 +384,10 @@ getPackageListR = do .| sinkList Just packages' -> do -- for each item in list get best available from version range - let vMap = (packageVersionId &&& packageVersionVersion) <$> packages' + let vMap = (packageReqId &&& packageReqVersion) <$> packages' runDB . runConduit - $ getPkgData (packageVersionId <$> packages') + $ getPkgData (packageReqId <$> packages') .| zipVersions .| mapC (\(a, vs) -> @@ -398,16 +405,16 @@ getPackageListR = do -- log all errors but just throw first error until Validation implemented - TODO https://hackage.haskell.org/package/validation for_ xs (\e -> $logWarn [i|Get package list errors: #{e}|]) sendResponseStatus (toStatus x) x - [] -> pure $ ServiceAvailableRes res + [] -> pure $ PackageListRes res where - defaults = ServiceListDefaults { serviceListOrder = DESC - , serviceListPageLimit = 20 - , serviceListPageNumber = 1 - , serviceListCategory = Nothing - , serviceListQuery = "" + defaults = PackageListDefaults { packageListOrder = DESC + , packageListPageLimit = 20 + , packageListPageNumber = 1 + , packageListCategory = Nothing + , packageListQuery = "" } - getPkgIdsQuery :: Handler (Maybe [PackageVersion]) + getPkgIdsQuery :: Handler (Maybe [PackageReq]) getPkgIdsQuery = lookupGetParam "ids" >>= \case Nothing -> pure Nothing Just ids -> case eitherDecodeStrict (encodeUtf8 ids) of @@ -427,7 +434,7 @@ getPackageListR = do Just t -> pure $ Just t getPageQuery :: Handler Int getPageQuery = lookupGetParam "page" >>= \case - Nothing -> pure $ serviceListPageNumber defaults + Nothing -> pure $ packageListPageNumber defaults Just p -> case readMaybe p of Nothing -> do let e = InvalidParamsE "get:page" p @@ -438,7 +445,7 @@ getPackageListR = do _ -> t getLimitQuery :: Handler Int getLimitQuery = lookupGetParam "per-page" >>= \case - Nothing -> pure $ serviceListPageLimit defaults + Nothing -> pure $ packageListPageLimit defaults Just pp -> case readMaybe pp of Nothing -> do let e = InvalidParamsE "get:per-page" pp @@ -475,12 +482,12 @@ createPackageMetadata pkgs = do & HM.fromListWith mergeDupes pure $ (keys, HM.intersectionWith (,) vers (categoryName <<$>> cats)) -getServiceDetails :: (MonadIO m, MonadResource m, MonadReader r m, MonadLogger m, Has AppSettings r, MonadUnliftIO m) +getServiceDetails :: (MonadResource m, MonadReader r m, MonadLogger m, Has AppSettings r, MonadUnliftIO m) => (Version -> Bool) -> ConnectionPool -> (HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) -> PkgId - -> m (Either S9Error ServiceRes) + -> m (Either S9Error PackageRes) getServiceDetails osPredicate appConnPool metadata pkg = runExceptT $ do settings <- ask packageMetadata <- case HM.lookup pkg metadata of @@ -502,23 +509,22 @@ getServiceDetails osPredicate appConnPool metadata pkg = runExceptT $ do case eitherDecode manifest of Left _ -> liftEither . Left $ AssetParseE [i|#{pkg}:manifest|] (decodeUtf8 $ BS.toStrict manifest) Right m -> do - let depVerList = - (fst &&& (serviceDependencyInfoVersion . snd)) <$> (HM.toList $ serviceManifestDependencies m) + let depVerList = (fst &&& (packageDependencyVersion . snd)) <$> (HM.toList $ packageManifestDependencies m) (_, depMetadata) <- lift $ runSqlPool (createPackageMetadata =<< getDependencies depVerList) appConnPool let (errors, deps) = partitionEithers $ parMap rpar (mapDependencyMetadata domain $ (HM.union depMetadata metadata)) - (HM.toList $ serviceManifestDependencies m) + (HM.toList $ packageManifestDependencies m) case errors of _ : xs -> liftEither . Left $ DepMetadataE xs - [] -> pure $ ServiceRes { serviceResIcon = [i|https://#{domain}/package/icon/#{pkg}|] + [] -> pure $ PackageRes { packageResIcon = [i|https://#{domain}/package/icon/#{pkg}|] -- pass through raw JSON Value, we have checked its correct parsing above - , serviceResManifest = unsafeFromJust . decode $ manifest - , serviceResCategories = snd packageMetadata - , serviceResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] - , serviceResLicense = [i|https://#{domain}/package/license/#{pkg}|] - , serviceResVersions = fst . fst $ packageMetadata - , serviceResDependencyInfo = HM.fromList deps + , packageResManifest = unsafeFromJust . decode $ manifest + , packageResCategories = snd packageMetadata + , packageResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] + , packageResLicense = [i|https://#{domain}/package/license/#{pkg}|] + , packageResVersions = fst . fst $ packageMetadata + , packageResDependencies = HM.fromList deps } where getDependencies :: (MonadResource m, MonadUnliftIO m) @@ -538,23 +544,23 @@ getServiceDetails osPredicate appConnPool metadata pkg = runExceptT $ do mapDependencyMetadata :: Text -> HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle]) - -> (PkgId, ServiceDependencyInfo) - -> Either Text (PkgId, DependencyInfo) + -> (PkgId, PackageDependency) + -> Either Text (PkgId, DependencyRes) mapDependencyMetadata domain metadata (appId, depInfo) = do depMetadata <- case HM.lookup appId metadata of Nothing -> Left [i|dependency metadata for #{appId} not found.|] Just m -> pure m -- get best version from VersionRange of dependency - let satisfactory = filter (<|| serviceDependencyInfoVersion depInfo) (fst . fst $ depMetadata) + let satisfactory = filter (<|| packageDependencyVersion depInfo) (fst . fst $ depMetadata) let best = getMax <$> foldMap (Just . Max) satisfactory version <- case best of Nothing -> Left [i|No satisfactory version for dependent package #{appId}|] Just v -> pure v pure ( appId - , DependencyInfo { dependencyInfoTitle = appId - , dependencyInfoIcon = [i|https://#{domain}/package/icon/#{appId}?spec==#{version}|] - } + , DependencyRes { dependencyResTitle = appId + , dependencyResIcon = [i|https://#{domain}/package/icon/#{appId}?spec==#{version}|] + } ) fetchAllAppVersions :: PkgId -> Handler ([VersionInfo], ReleaseNotes) diff --git a/src/Lib/PkgRepository.hs b/src/Lib/PkgRepository.hs index fc4db82..e826228 100644 --- a/src/Lib/PkgRepository.hs +++ b/src/Lib/PkgRepository.hs @@ -37,7 +37,7 @@ import qualified Data.Text as T import Lib.Error ( S9Error(NotFoundE) ) import qualified Lib.External.AppMgr as AppMgr import Lib.Types.AppIndex ( PkgId(..) - , ServiceManifest(serviceManifestIcon) + , PackageManifest(packageManifestIcon) ) import Lib.Types.Emver ( Version , VersionRange @@ -163,7 +163,7 @@ extractPkg fp = handle @_ @SomeException cleanup $ do liftIO . throwIO $ ManifestParseException (pkgRoot "manifest.json") Right manifest -> do wait iconTask - let iconDest = "icon" <.> case serviceManifestIcon manifest of + let iconDest = "icon" <.> case packageManifestIcon manifest of Nothing -> "png" Just x -> case takeExtension (T.unpack x) of "" -> "png" diff --git a/src/Lib/Types/AppIndex.hs b/src/Lib/Types/AppIndex.hs index 7ffdd62..a12f296 100644 --- a/src/Lib/Types/AppIndex.hs +++ b/src/Lib/Types/AppIndex.hs @@ -8,6 +8,7 @@ module Lib.Types.AppIndex where import Startlude +-- NOTE: leave eitherDecode for inline test evaluation below import Control.Monad ( fail ) import Data.Aeson ( (.:) , (.:?) @@ -16,6 +17,7 @@ import Data.Aeson ( (.:) , ToJSON(..) , ToJSONKey(..) , withObject + , eitherDecode ) import qualified Data.ByteString.Lazy as BS import Data.Functor.Contravariant ( contramap ) @@ -76,43 +78,43 @@ data VersionInfo = VersionInfo deriving (Eq, Show) -- TODO rename to PackageDependencyInfo -data ServiceDependencyInfo = ServiceDependencyInfo - { serviceDependencyInfoOptional :: Maybe Text - , serviceDependencyInfoVersion :: VersionRange - , serviceDependencyInfoDescription :: Maybe Text - , serviceDependencyInfoCritical :: Bool +data PackageDependency = PackageDependency + { packageDependencyOptional :: Maybe Text + , packageDependencyVersion :: VersionRange + , packageDependencyDescription :: Maybe Text + , packageDependencyCritical :: Bool } deriving Show -instance FromJSON ServiceDependencyInfo where +instance FromJSON PackageDependency where parseJSON = withObject "service dependency info" $ \o -> do - serviceDependencyInfoOptional <- o .:? "optional" - serviceDependencyInfoVersion <- o .: "version" - serviceDependencyInfoDescription <- o .:? "description" - serviceDependencyInfoCritical <- o .: "critical" - pure ServiceDependencyInfo { .. } + packageDependencyOptional <- o .:? "optional" + packageDependencyVersion <- o .: "version" + packageDependencyDescription <- o .:? "description" + packageDependencyCritical <- o .: "critical" + pure PackageDependency { .. } data ServiceAlert = INSTALL | UNINSTALL | RESTORE | START | STOP deriving (Show, Eq, Generic, Hashable, Read) -data ServiceManifest = ServiceManifest - { serviceManifestId :: !PkgId - , serviceManifestTitle :: !Text - , serviceManifestVersion :: !Version - , serviceManifestDescriptionLong :: !Text - , serviceManifestDescriptionShort :: !Text - , serviceManifestReleaseNotes :: !Text - , serviceManifestIcon :: !(Maybe Text) - , serviceManifestAlerts :: !(HM.HashMap ServiceAlert (Maybe Text)) - , serviceManifestDependencies :: !(HM.HashMap PkgId ServiceDependencyInfo) +data PackageManifest = PackageManifest + { packageManifestId :: !PkgId + , packageManifestTitle :: !Text + , packageManifestVersion :: !Version + , packageManifestDescriptionLong :: !Text + , packageManifestDescriptionShort :: !Text + , packageManifestReleaseNotes :: !Text + , packageManifestIcon :: !(Maybe Text) + , packageManifestAlerts :: !(HM.HashMap ServiceAlert (Maybe Text)) + , packageManifestDependencies :: !(HM.HashMap PkgId PackageDependency) } deriving Show -instance FromJSON ServiceManifest where +instance FromJSON PackageManifest where parseJSON = withObject "service manifest" $ \o -> do - serviceManifestId <- o .: "id" - serviceManifestTitle <- o .: "title" - serviceManifestVersion <- o .: "version" - serviceManifestDescriptionLong <- o .: "description" >>= (.: "long") - serviceManifestDescriptionShort <- o .: "description" >>= (.: "short") - serviceManifestIcon <- o .: "assets" >>= (.: "icon") - serviceManifestReleaseNotes <- o .: "release-notes" + packageManifestId <- o .: "id" + packageManifestTitle <- o .: "title" + packageManifestVersion <- o .: "version" + packageManifestDescriptionLong <- o .: "description" >>= (.: "long") + packageManifestDescriptionShort <- o .: "description" >>= (.: "short") + packageManifestIcon <- o .: "assets" >>= (.: "icon") + packageManifestReleaseNotes <- o .: "release-notes" alerts <- o .: "alerts" a <- for (HM.toList alerts) $ \(key, value) -> do alertType <- case readMaybe $ T.toUpper key of @@ -120,12 +122,12 @@ instance FromJSON ServiceManifest where Just t -> pure t alertDesc <- parseJSON value pure (alertType, alertDesc) - let serviceManifestAlerts = HM.fromList a - serviceManifestDependencies <- o .: "dependencies" - pure ServiceManifest { .. } + let packageManifestAlerts = HM.fromList a + packageManifestDependencies <- o .: "dependencies" + pure PackageManifest { .. } --- >>> 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})]}) +-- >>> eitherDecode testManifest :: Either String PackageManifest +-- Right (PackageManifest {packageManifestId = embassy-pages, packageManifestTitle = "Embassy Pages", packageManifestVersion = 0.1.3, packageManifestDescriptionLong = "Embassy Pages is a simple web server that uses directories inside File Browser to serve Tor websites.", packageManifestDescriptionShort = "Create Tor websites, hosted on your Embassy.", packageManifestReleaseNotes = "Upgrade to EmbassyOS v0.3.0", packageManifestIcon = Just "icon.png", packageManifestAlerts = fromList [(INSTALL,Nothing),(UNINSTALL,Nothing),(STOP,Nothing),(RESTORE,Nothing),(START,Nothing)], packageManifestDependencies = fromList [(filebrowser,PackageDependency {packageDependencyOptional = Nothing, packageDependencyVersion = >=2.14.1.1 <3.0.0, packageDependencyDescription = Just "Used to upload files to serve.", packageDependencyCritical = False})]}) testManifest :: BS.ByteString testManifest = [i|{ "id": "embassy-pages", diff --git a/test/Handler/AppSpec.hs b/test/Handler/AppSpec.hs index 1f7c4e5..b3431d6 100644 --- a/test/Handler/AppSpec.hs +++ b/test/Handler/AppSpec.hs @@ -17,6 +17,8 @@ import Seed import Lib.Types.AppIndex import Data.Aeson import Data.Either.Extra +import Handler.Marketplace ( PackageListRes ) + spec :: Spec spec = do describe "GET /package/index" $ withApp $ it "returns list of packages" $ do @@ -25,7 +27,7 @@ spec = do setMethod "GET" setUrl ("/package/index" :: Text) statusIs 200 - (res :: [ServiceRes]) <- requireJSONResponse + (res :: PackageListRes) <- requireJSONResponse assertEq "response should have two packages" (length res) 3 describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version" $ do _ <- seedBitcoinLndStack @@ -33,11 +35,11 @@ spec = do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\"=0.21.1.2\"}]" :: Text) statusIs 200 - (res :: [ServiceRes]) <- requireJSONResponse + (res :: PackageListRes) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg - assertEq "manifest id should be bitcoind" (serviceManifestId manifest) "bitcoind" + let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + assertEq "manifest id should be bitcoind" (packageManifestId manifest) "bitcoind" describe "GET /package/index?ids" $ withApp $ it "returns list of packages and dependencies at specified version" @@ -47,43 +49,43 @@ spec = do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"lnd\",\"version\":\"=0.13.3.1\"}]" :: Text) statusIs 200 - (res :: [ServiceRes]) <- requireJSONResponse + (res :: PackageListRes) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - assertEq "package dependency metadata should not be empty" (null $ serviceResDependencyInfo pkg) False + assertEq "package dependency metadata should not be empty" (null $ packageResDependencyInfo pkg) False describe "GET /package/index?ids" $ withApp $ it "returns list of packages at exactly specified version" $ do _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\"=0.21.1.1\"}]" :: Text) statusIs 200 - (res :: [ServiceRes]) <- requireJSONResponse + (res :: PackageListRes) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg - assertEq "manifest version should be 0.21.1.1" (serviceManifestVersion manifest) "0.21.1.1" + let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + assertEq "manifest version should be 0.21.1.1" (packageManifestVersion manifest) "0.21.1.1" describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version or greater" $ do _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\">=0.21.1.1\"}]" :: Text) statusIs 200 - (res :: [ServiceRes]) <- requireJSONResponse + (res :: PackageListRes) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg - assertEq "manifest version should be 0.21.1.2" (serviceManifestVersion manifest) "0.21.1.2" + let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + assertEq "manifest version should be 0.21.1.2" (packageManifestVersion manifest) "0.21.1.2" describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version or greater" $ do _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\">=0.21.1.2\"}]" :: Text) statusIs 200 - (res :: [ServiceRes]) <- requireJSONResponse + (res :: PackageListRes) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - let (manifest :: ServiceManifest) = fromRight' $ eitherDecode $ encode $ serviceResManifest pkg - assertEq "manifest version should be 0.21.1.2" (serviceManifestVersion manifest) "0.21.1.2" + let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + assertEq "manifest version should be 0.21.1.2" (packageManifestVersion manifest) "0.21.1.2" describe "GET /package/:pkgId with unknown version spec for bitcoind" $ withApp $ it "fails to get unknown app" $ do _ <- seedBitcoinLndStack request $ do From 85f080a86f6ba580caa8349f2b64e18cb1289b18 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Mon, 22 Nov 2021 13:06:31 -0700 Subject: [PATCH 06/12] update server name --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a05e085..cd63d16 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ After installing Postgres, run: ``` -createuser start9-companion-server --pwprompt --superuser -# Enter password start9-companion-server when prompted -createdb start9-companion-server -createdb start9-companion-server_test +createuser start9-registry --pwprompt --superuser +# Enter password start9-registry when prompted +createdb start9-registry +createdb start9-registry_test ``` ## Haskell Setup @@ -44,11 +44,15 @@ To create `hie.yaml` if it does not exist: ## Tests ``` -stack test --flag start9-companion-server:library-only --flag start9-companion-server:dev +stack test --flag start9-registry:library-only --flag start9-registry:dev ``` (Because `yesod devel` passes the `library-only` and `dev` flags, matching those flags means you don't need to recompile between tests and development, and it disables optimization to speed up your test compile times). +## Builds + +`stack build --copy-bins --local-bin-path=dist` + ### Tests with HIE Setup - install hspec-discover globally `cabal install hspec-discover` (requires cabal installation) - Current [issue](https://github.com/haskell/haskell-ide-engine/issues/1564) open for error pertaining to obtaining flags for test files From 89cbabe8a191090e8ffef946a50fb666e75064c3 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Mon, 22 Nov 2021 13:07:13 -0700 Subject: [PATCH 07/12] add testing resources directory --- resources/apps/bitcoind/0.21.1.1/hash.bin | 1 + resources/apps/bitcoind/0.21.1.1/icon.png | Bin 0 -> 13469 bytes .../apps/bitcoind/0.21.1.1/instructions.md | 23 ++ resources/apps/bitcoind/0.21.1.1/license.md | 22 ++ .../apps/bitcoind/0.21.1.1/manifest.json | 272 ++++++++++++++ resources/apps/bitcoind/0.21.1.2/hash.bin | 1 + resources/apps/bitcoind/0.21.1.2/icon.png | Bin 0 -> 13469 bytes .../apps/bitcoind/0.21.1.2/instructions.md | 23 ++ resources/apps/bitcoind/0.21.1.2/license.md | 22 ++ .../apps/bitcoind/0.21.1.2/manifest.json | 272 ++++++++++++++ resources/apps/btc-rpc-proxy/0.3.2.1/hash.bin | 1 + resources/apps/btc-rpc-proxy/0.3.2.1/icon.png | Bin 0 -> 38948 bytes .../btc-rpc-proxy/0.3.2.1/instructions.md | 17 + .../apps/btc-rpc-proxy/0.3.2.1/license.md | 21 ++ .../apps/btc-rpc-proxy/0.3.2.1/manifest.json | 260 +++++++++++++ resources/apps/lnd/0.13.3.1/hash.bin | 1 + resources/apps/lnd/0.13.3.1/icon.png | Bin 0 -> 4201 bytes resources/apps/lnd/0.13.3.1/instructions.md | 52 +++ resources/apps/lnd/0.13.3.1/license.md | 19 + resources/apps/lnd/0.13.3.1/manifest.json | 348 ++++++++++++++++++ 20 files changed, 1355 insertions(+) create mode 100644 resources/apps/bitcoind/0.21.1.1/hash.bin create mode 100644 resources/apps/bitcoind/0.21.1.1/icon.png create mode 100644 resources/apps/bitcoind/0.21.1.1/instructions.md create mode 100644 resources/apps/bitcoind/0.21.1.1/license.md create mode 100644 resources/apps/bitcoind/0.21.1.1/manifest.json create mode 100644 resources/apps/bitcoind/0.21.1.2/hash.bin create mode 100644 resources/apps/bitcoind/0.21.1.2/icon.png create mode 100644 resources/apps/bitcoind/0.21.1.2/instructions.md create mode 100644 resources/apps/bitcoind/0.21.1.2/license.md create mode 100644 resources/apps/bitcoind/0.21.1.2/manifest.json create mode 100644 resources/apps/btc-rpc-proxy/0.3.2.1/hash.bin create mode 100644 resources/apps/btc-rpc-proxy/0.3.2.1/icon.png create mode 100644 resources/apps/btc-rpc-proxy/0.3.2.1/instructions.md create mode 100644 resources/apps/btc-rpc-proxy/0.3.2.1/license.md create mode 100644 resources/apps/btc-rpc-proxy/0.3.2.1/manifest.json create mode 100644 resources/apps/lnd/0.13.3.1/hash.bin create mode 100644 resources/apps/lnd/0.13.3.1/icon.png create mode 100644 resources/apps/lnd/0.13.3.1/instructions.md create mode 100644 resources/apps/lnd/0.13.3.1/license.md create mode 100644 resources/apps/lnd/0.13.3.1/manifest.json diff --git a/resources/apps/bitcoind/0.21.1.1/hash.bin b/resources/apps/bitcoind/0.21.1.1/hash.bin new file mode 100644 index 0000000..64bc77f --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.1/hash.bin @@ -0,0 +1 @@ +JAPEX7ZLZIBZUO6YLUQAAGYACA4E5IFBLJPZR3OVQNQOAPNYVHVIEVXRS5RZ65EDCAAQE65PIVOZ46Z6CR4KIX3MWL62YFU5IXBHD4I diff --git a/resources/apps/bitcoind/0.21.1.1/icon.png b/resources/apps/bitcoind/0.21.1.1/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c6dc195a6e2b9fe93eb2d8b8a6377cbfd0b57250 GIT binary patch literal 13469 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 LNkvXXu0mjf6an;> literal 0 HcmV?d00001 diff --git a/resources/apps/bitcoind/0.21.1.1/instructions.md b/resources/apps/bitcoind/0.21.1.1/instructions.md new file mode 100644 index 0000000..358899e --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.1/instructions.md @@ -0,0 +1,23 @@ +# Bitcoin Core + +## Getting Started + +### Config + +Your node is highly configurable. Many settings are considered _advanced_ and should be used with caution. For the vast majority of users and use-cases, we recommend using the defaults. This is where you can change RPC credentials as well. Once configured, you may start your node! + +### Syncing + +Depending on your Internet bandwidth, your node should take approximately 5-7 days to sync from genesis to present. + +### Using a Wallet + +Enter your QuickConnect QR code **OR** your raw RPC credentials (both located in `Properties`) into any wallet that supports connecting to a remote node over Tor. For a full list of compatible wallets, see https://github.com/start9labs/bitcoind-wrapper/blob/master/docs/wallets.md. + +## Pruning + +Pruning is a process by which your node discards old blocks and transactions after it verifies them. Pruned nodes and archival nodes are both "full nodes" in that they are fully validating - they validate every block and transaction. Archival nodes store the entire blockchain and are useful to people interested in doing general or historical analysis, or being a provider of blockchain data to others (eg. a blockexplorer). + +The target of pruning on your Embassy is configurable and set by default to the minimum of 550MB (0.55 GB!), meaning the resulting blockchain will occupy a negligible amount of storage space. The maximum amount of blockchain data you can retain depends on the storage capacity your device. The config menu will not permit you to select a target that exceeds a certain percentage of your device's available capacity. + +For most use cases, we recommend sticking with a very low pruning setting. diff --git a/resources/apps/bitcoind/0.21.1.1/license.md b/resources/apps/bitcoind/0.21.1.1/license.md new file mode 100644 index 0000000..461bc73 --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.1/license.md @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2009-2020 The Bitcoin Core developers +Copyright (c) 2009-2020 Bitcoin Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/resources/apps/bitcoind/0.21.1.1/manifest.json b/resources/apps/bitcoind/0.21.1.1/manifest.json new file mode 100644 index 0000000..aa5bf9b --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.1/manifest.json @@ -0,0 +1,272 @@ +{ + "id": "bitcoind", + "title": "Bitcoin Core", + "version": "0.21.1.1", + "description": { + "short": "A Bitcoin Full Node by Bitcoin Core", + "long": "Bitcoin is an innovative payment network and a new kind of money. Bitcoin uses peer-to-peer technology to operate with no central authority or banks; managing transactions and the issuing of bitcoins is carried out collectively by the network. Bitcoin is open-source; its design is public, nobody owns or controls Bitcoin and everyone can take part. Through many of its unique properties, Bitcoin allows exciting uses that could not be covered by any previous payment system." + }, + "assets": { + "license": "LICENSE", + "instructions": "instructions.md", + "icon": "icon.png", + "docker-images": "image.tar", + "assets": null + }, + "build": [ + "make" + ], + "release-notes": "Upgrade to EmbassyOS v0.3.0", + "license": "mit", + "wrapper-repo": "https://github.com/Start9Labs/bitcoind-wrapper", + "upstream-repo": "https://github.com/bitcoin/bitcoin", + "support-site": "https://github.com/bitcoin/bitcoin/issues", + "marketing-site": "https://bitcoincore.org/", + "donation-url": null, + "alerts": { + "install": null, + "uninstall": "Uninstalling Bitcoin Core will result in permanent loss of data. Without a backup, any funds stored on your node's default hot wallet will be lost forever. If you are unsure, we recommend making a backup, just to be safe.", + "restore": "Restoring Bitcoin Core will overwrite its current data. You will lose any transactions recorded in watch-only wallets, and any funds you have received to the hot wallet, since the last backup.", + "start": null, + "stop": null + }, + "main": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "docker_entrypoint.sh", + "args": [], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "health-checks": { + "rpc": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "bitcoin-cli", + "args": [ + "-rpcconnect=bitcoind.embassy", + "-getinfo" + ], + "mounts": {}, + "io-format": "yaml", + "inject": true, + "shm-size-mb": null, + "critical": false + }, + "synced": { + "type": "docker", + "image": "utils", + "system": true, + "entrypoint": "/mnt/assets/check-synced.sh", + "args": [], + "mounts": { + "main": "/root/.bitcoin", + "utils": "/mnt/assets" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null, + "critical": false + } + }, + "config": { + "get": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "get", + "/root/.bitcoin", + "/mnt/assets/config_spec.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "set": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "set", + "bitcoind", + "/root/.bitcoin", + "/mnt/assets/config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + }, + "properties": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "properties", + "/root/.bitcoin" + ], + "mounts": { + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "volumes": { + "compat": { + "type": "assets" + }, + "main": { + "type": "data" + }, + "utils": { + "type": "assets" + } + }, + "min-os-version": "0.3.0", + "interfaces": { + "peer": { + "name": "Peer Interface", + "description": "Listens for incoming connections from peers on the bitcoin network", + "tor-config": { + "port-mapping": { + "8333": "8333" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "bitcoin" + ] + }, + "rpc": { + "name": "RPC Interface", + "description": "Listens for JSON-RPC commands", + "tor-config": { + "port-mapping": { + "8332": "8332" + } + }, + "lan-config": { + "8332": { + "ssl": false, + "mapping": 8332 + } + }, + "ui": false, + "protocols": [ + "tcp", + "http", + "json-rpc" + ] + }, + "zmq": { + "name": "ZeroMQ Interface", + "description": "Listens for subscriptions to the ZeroMQ raw block and raw transaction event streams", + "tor-config": { + "port-mapping": { + "28332": "28332", + "28333": "28333" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "zmq" + ] + } + }, + "backup": { + "create": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "create", + "/mnt/backup", + "/root/.bitcoin" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/.bitcoin" + }, + "io-format": null, + "inject": false, + "shm-size-mb": null + }, + "restore": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "restore", + "/mnt/backup", + "/root/.bitcoin" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/.bitcoin" + }, + "io-format": null, + "inject": false, + "shm-size-mb": null + } + }, + "migrations": { + "from": {}, + "to": {} + }, + "actions": { + "reindex": { + "name": "Reindex Blockchain", + "description": "Rebuilds the block and chainstate databases starting from genesis. If blocks already exist on disk, these are used rather than being redownloaded. However, since embassy bitcoin nodes are pruned by default, this usually means downloading the entire blockchain over again.", + "warning": "Blocks not stored on disk will be redownloaded in order to rebuild the database. If your node is pruned (embasssy nodes are pruned by default), this action is equivalent to syncing the node from scratch, so this process could take a couple of weeks.", + "implementation": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "reindex.sh", + "args": [], + "mounts": { + "main": "/root/.bitcoin" + }, + "io-format": "json", + "inject": false, + "shm-size-mb": null + }, + "allowed-statuses": [ + "running", + "stopped" + ], + "input-spec": {} + } + }, + "dependencies": {} +} \ No newline at end of file diff --git a/resources/apps/bitcoind/0.21.1.2/hash.bin b/resources/apps/bitcoind/0.21.1.2/hash.bin new file mode 100644 index 0000000..64bc77f --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.2/hash.bin @@ -0,0 +1 @@ +JAPEX7ZLZIBZUO6YLUQAAGYACA4E5IFBLJPZR3OVQNQOAPNYVHVIEVXRS5RZ65EDCAAQE65PIVOZ46Z6CR4KIX3MWL62YFU5IXBHD4I diff --git a/resources/apps/bitcoind/0.21.1.2/icon.png b/resources/apps/bitcoind/0.21.1.2/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c6dc195a6e2b9fe93eb2d8b8a6377cbfd0b57250 GIT binary patch literal 13469 zcmV;OG-At%P)!9WJXV3g`uuf(sY9>SgHy_KH{i>otga!3Ci$YEiF> ziV7+sUN%wCvP0P^TlY3en=Way&Sd7i&;NPPKxx_}otexzGn0A09}bzBoMw_U=Y8I1 ze*lZcVzF2(#RtJsbg=o{e>SxgogF1xVqv#-%D6aIu1ru9G%Q7h6#xYeFGgvvuCIks z4J8gwh`V|WfyOeUh6octG>*Yz<qzz6#{^ z=f(;7^?F1IbSrSH{JyV=2J#eXe$i#Uvf|3NHu*akmb}0UfE>fi>dSj$d?>MVuqfq6 ziYk7Bd?*v;lc|<(M=&xlW=5Bo7aj6&wU`=Di$Hr?JBguL9kP<%WCmuJg1586TjrMk?mX7TkXJKtN`!{ zz8y}irQtww_(;)zKPsyIiE^=PVA($`PDX{*tmzpheIGgQ6V_Dl2P*)wOwY{?lgOph z#8W?4?A~|FC9a1hbBx1~u}L(9XC%7uTTOlctem&inh9CL3Vquz3clVg*2o@b%+I^_G<$LZmO2%XY53R0m7Gu|vec9g67YlI@#bu};JN zV+Fu|Fbh9gS$Y&SUn&>tY}r~WVaa!*@>$)h+5L8xqhGF>x3vqF0kHyL5cuW^l|9-y zNkLsCPd^0Mn0>(^;Y1PG_lFhT6bkFd>C)C7So&cFz+P}ueR=m-{7Z;vf!LMrlPhE$ zdxcL_Y=L_f>zlSELQhRvw5b;s4=Vunpy&F=iB!(l%fk<2v~wM05CMmu0M7>eV&=;k@f12cuWY9!T+;)Tj}?wHLZ)Hh$(6TUt7D2mt=s=+S*$Ws6+U74ipkxuhYWjO(XPz&QK*sOL&1 z`UW$HVX@=^vXG*3KVBQzG4tEh^l$`L_5?E!E_NUmOMslzGa8&TvZ6I#|lCK@UrREvA*8_6~EtyVX+hd ziuf1py?S0HA=@6c)d8-NcoZSG8?Ho|4xBp!Wwg)8U5y zS9E|=V6hY?k9Nn^1>+ZOYDONSkf-eYLG>6ULbuq(Zz*t2tSr$tn>iE6L-JAp@Zzym zrEdAJL_c{CES3U?8UGs9p<7J%Y2+DsX95^W*{j31$}e^vES8Z3^N%B_SddR4!o1P| znwQm=_i<>MUHq023DIX`=>8VpJ}YmgP34USz{`#d$77qXbBPzhVi~EB=mDB;Dnr>9 z^Fb5H3k`t(1L5Aj)+=4`Rj^n_GHfKTj^kO(F^GXf7Y2vau*R_jaJ7 z4&2oW-SQ-K>r>F%UIE%on>!6lu*l}O#c>kvE4`xaMdXN_3V@!)jmIhCpUQ=p1&goH zxLGK@;9gG?*bOJZvCgDGFv4KV^I#DO+`bBM`(OzG!mHdDdvE!o?Kw#l%9#L=#Xpf0 z|6CrPjy$l1Mmo-+7bAS?w}w9HZGq6U6?*duPbjc#PQ2qy`CdVlxfc|gabI~Ue}pb= z?Lolg#O7|3Sid+!zb5|BQ|#go0NM3b!_oUckHk+983Q$T212twhLY32i<0-;;Js$? z4{->*KN`mkFE=6Z@L=Eqz<53@-0)l?e9TVj2?(bYYWyKc8*;$Wb~$#!;x7^!ykOec z#!C<|feV1{#r0=8ge7A4SHKboR8b2Wo!LB8V+MFrvQk2M+5!3R}0+K61d3`r z9+_aG0cHWG{5}oGOSo~Icc_}ff!Y5C?%g2*VFUE`*TKzCh(l4nia_94yA=onK{$qE z^i^)cJZggPp_sq~z!Y>}>Jps;OR$71WHHV`Cg?T^3wEX6%VHowWAYEo;R%F;FA#si z4xm%QjyvD<1cGjRNd&@r&(~n_2odr5(Qyrr0WCj+&jjKF=v&-4#|3>3HU%wksBAo_ zdI~auw>+1I{ZC?QynFv=MMNM>I0Vi?=OT3KbtpOaH*h54V0j0UPmCB>i77A{KI6Lp z;Kc`4X~O@=Uo{n$z)>dsHF<))gtAPF0iSww;Q!DH$c^z%E$&!-Uxsr>`YV zXaDe30GNnq7hyXMEm(=MvEA71cs z3IB~f5Sd_l1f*{Ni=(mrC}aY6ZnE}&>P#9{UV5#3&1%=ItP6l%FPE9^2(LX*^+zTb zzrhsdNW9#OnICZ(!aCu3!QB=S2%DdVZh0arHnFOj464j5>5^NXO3VIN6EY|?g0=tC zMWSi*VvFl;DP6cZYhjbD3IIMIPA}ArHruB^s^Nqoz8M159EMPhahH6?v%k6hO=!^$ zykqkUuVF{J7_B2S`=3P^J(2qP#{M6bS-y(-`z+}|Ar1PTJjj|qAgcl(dXnkwhs@$m zBNQ4X3#2-Y1wu+x_nC0aG5b^AOI|_E+XA8au@nS?*#DWv|97pm_J2M@cz@55`j3|{ zXuSCuxy<`6m7g6acR0n68InER_y+<5~o!}195L%uVf$);&V5IU1z^Kdw zKsxpRp8Y>AL;HW{TG3>Neg76EBI*xU9KM*F@EvLzUN_gQ2!QU`##svCi$S)$b8l4R z3GfRtAw@Y%!w*$|tfvKVyi0^ahb+!aB1Yn7YEHxcFRv91C(}kFsV5p)_JK(AWzli9 zw*y<&Vi*iN55SB$3MuDuA|4A%Kgf9E#~gsb5L=u|MtOdRObRc#vn9Ry|N0CYjU=MU zN<&NL5KRR;pSoQf?-&Nd769?K_CqzR3t-s?)o>grG8%;fryc)Kq0#6-?_=!$4jca; zHiSz)bW+V!41-|{fP_n*CUD{0i7LD86l5IXUzPzRioY$ECjuR%14OdE~D z2!Lj5|7V9uT8BQ&!_WeTW?mp>XV&wRM*>t) zmvJZFoMU_mOh^dysrGaaj6e8jT{}4NLYAFU?`(hR@&(LO&Tf4|ws| zDi`6U@^CyX1LEcLulu`~sYgZ=2v>ldus3a|m~mhFnU}$6$tq$AsH)J}_{%l-^!29m z57RaQjFgFS*~gi9CM<(u25g!GEl?`u{a9Fa3cD*uOcg=mW_!X3?yTYLR<)QBrONy3I1mRP!gY&NQQFMp^Fzv|P=YJw5!XK@l zAR~B<+nYGC@(Zbz3a1qYbQq_=E`EOzH8}C?59MnNMTa;C)r8pyzxPIzp8pp(2b^6L zevvFQRh2s_4{1nQ0PxSIMijvgaSMQC2cSn$yoh;FGY8=hEJMkK4KU!ANv_> z)oVSDzAh9>VV9b68bW7$A5=XJmVsgLoR|~cZ)3=m90hVao-RMp7XAc9NhMUxZ1{~{ z_aNerG$nfrmQ;KF{Zzxzo+sX8NAud7qnHWdof07 z_YC>EYN(U;YfKsSKh{%vw@8#zU~9mAQ&Ud&Ivpuf?wkjOOs0k+bn5?r zA6%GTN8=(uoPkI5)-@gh3_gNszXCux&M|D&{{SJcnW*3J_SeAC_7qRn;NIX}}UX`5Mtws$qEt zk=^eds!_<00wB7yb}ESdcSl=xrY>P<2Xz_`Cat=d#Mrl}EPO1Y}Yk_DU+ zsCxg)E%e|{;q1Lf>(hvf5O7!C=Cu?L5@*U8P~&XJy0_7Ue|fnHB?EnLPyx_AHg2w< zZTcA?rbi(8edD_E^TRvdt-A`yeQm;`#mKM*9!%q|F(!QxTflZX%K7#UOmDCHgn zqGD)Hmp24r|L=TLJog(AD1DuXKlBCP{0;-5Mo*JtDl8rm#cDv?azFv##s4+=pN(R9 z2PZ7{|55OpAngBvFcX0j&;B5|TmdtOq2>r!-h%1Gf${+c6ac+Hn%E$_|Ddr$OZK7C zF-O>K4JZIq+&x5rt6&KXReLy4?yt|) z*#Erk732&t1@xxn2nwp6;w8jeysdwfqnTU0n2`}7AGPyo0t9}2el|A8U)s;c+D@uhCgeV;*-F$g#1!8OA} zg+U>k5cC#~R{N&)4=4bL$mahC2#uO3ftvj`Zy6prw*TLT1T0M{}urI z2!Ot@YrOw90VqJI@dWsdiT#^UT&|(YdQr;*O3>-+w!=sFKxFS|R88(T*hdX;k~zV) z{|*YV|D6D;*SIyn?7tyU#@q>L_$f!Xd!3IgNhBar))|U7?)loj0st@?fUN)w2+#hn zY=Ez5vH#niLq0%t!OfQ8&K6ku5eX63?fJsKPC#Yr|AR!0nGUL`fu9&Pn78KM{-?^i zK*F7G!&_g1r5_G7`X%`7D**O7!QwAu!he2ys=HdCcdkROQB%(lXJ?<)YvPX7&za2eF->ApA3@cRGd*cSF0bsTYdV3~Q`wK>q@qe4LN5C5rBZ8%0@YA;VX$4Si)g&BFG75iqFC4!uAMu2rWgjZAY|QRDpMF~p zX}4C9Q^Ix%NFbOoT=2W)f|wcMQ@#b39Z_$7HrWkGY-4Sz^88N8jf_F);0q8s{)>V3 z=rf&&^u}NtkQ5FajwM27vZ~j$wcp+6kQyV4zXTTlG4=m-{@4E-_1lCR&7m4k^iqON ze4KY}@Js`*KftcF(A{mKE%at5bI!~*Y&agFV-^K6<6v*htiXJ5dgdn-)?8u)fHyO+ ze_EhcqgZL1JRJbu+`Ry8;@ybgp^7^1Pz}cbM_eYha+^5J+Pq9blP;{ct(3??YqEUU zm=B3A&y*Mx(PF4#f|uoI7QQ<0EGScp@8lqzM6??B^*>tme-Om@59xx`dF>h%x7GALp3Iqz;f(N6%d%T*GkvCBUIRLva!?f*9&3&-z6HNU>>2YXyMc1ZDph6Gm&<@bFt>!YxDK z5Yg@dimLy2;BQZAi-%|b+m5wa#Uwi=p1MKImM&O^Mznz4<(T~wfm~Y#90XK7*&AtX z$qvSO8DIW2XfvVMvVWKd764-V+5SlZpeCM_8xQ#i#8mtx{_z!wUc3xTYKV(xx2LwB z0C2%p0QikueKpCBT+9X>-HG@=z65vOgTc)k7zyw@0-%2$0Gog=EIvZj(?C@|_x~#b zxP3L^f4j=dZnUI_iSfMsxPKl1aadUi;43u#5K!qTSTZCLMdG<1%JC!X@XHw9z3%&K z0A?1T)uepI%f7QC7t<%vi^OZchbxPpcddtIIMD9;^Zs|Dodw`8#{O^czyCjPf87js z@v;pIx-GV8WxjnS^y+(%c;(kXv>le=!{FU{()|ShA!;=M|1i1#&pb4`*p`+@L2Gtx63Kzi0Y}Rn!_#+a_uMtPz4p_1bV_*${HOI)#Qu7TObCB=$ zzc>1;V`K7h?{+UcPH+5YGH>4qgxWc9j36+A!89XzE8RD*AO$u3{Rl;OA^ze|0ju(7 z7sZ2^0HmU@Du8d8UD$8?pJnlz8G!v?)L>KCBM0wV2mQuf@@td>rBxyj4u>=4G;cJy ziBRNAegcIn;mr9o)IlE-yZ;s>Uj8`{?|~%?@E|?_+54lg_=K_lkAvTsa$b&g?*F`m zEG9WvOa;C60Z@r(CKdH?CY|6VDv>xVsWkVQ;ikecbi$XQ4mcC>2R~;UeP#ioch>n{ zPC$-YH=sXw6LEac|7YmtCy=%lXEcP^EvP-1f_GQ^c&-);f0*PuXdcaxv6)dRAO8h@Do z@qe$Y&qibq_B!jB0UL?!uR=>GP95~GaKtXB>gj>C_}&u=mm_T2i_7ojKKEnKjIpE+ zc^Ut_>+R?Ls{uvsn%F2Rdjiu zjty{MxCQb1E<@s_pNsDx2NQTj69^sm1vuiPu$h%B=C1oT@j)h{O|v#w>;~g{l8yC8`!lDeGguhY{?xk&h!s}>Vq3Q zUdTwKZ)fF#&AVr&^%DTqm+vy+fgBigBNr+g3mS7E{6*Y4X5vq-vp6Mu82{&&p*Jo^ zpoB^h5~m;{wxwkXY?9#3?i;0l9)Ns9{#y%6)}ZmTfKvbMe{CuN^MSrD^xc02bo*)q zi79w;4*Vc2L&F*y_I$qYcyK10YnesVggIX9+E0?{zq^n(M0X&u{A(uP7=d9*t-XkV zf@KI4^{cGf_i8D&0w9a1Mw9>VZ^De;`a<3n%IFOb!CiF^g2XY|mX)^K@}L;ipWLA} z0PV8L_jF+yHlF=&N^X6ITk^pE9{_jdufajK?K0!ZYJ(p}S||`B3fI}R=WA-;lV~&8 zRt07QGSh$jw*O^;>20qfA2E}Md1n@C6HDS_6nws5Rz9ScZ*B z|MlPgUzr@-nU81}di}!)68}3c6)=p>jTG6B6Yw+5&GO!Fn0>v~xjrYRRAJJbsnBx0r00X8? zh1(Oa%k5R14cSXZLA@(Y>&IlDg(89PJi*le4+rvEp#_O4i1IohTlzsttt+3ut#$9$ z2NVF*w6#las~1p&I8)D%U28J(o!1Rl?1~ms zG*C2^fi!jooCD86L0|@X?A$1yUVcVtP2!X@<6Hy{cQ$8dF2V?dh##e<)&bSU(BS__ z9I8v`uX3xI>WUFRC%~EgDGBY3MPWb)pZ*=_jTUK)97h%%rW_N zQGH{w`)%?ZU?xM+WGeE`wMmCqKxw!pRoXfHvj`n}u}IV0XeN5oBgvc$%R4YSMJ_)! z*tdri0I^VLMXBqG8Nef20H_)NTG*qe#BV{P5AY6U%3;kkpRUbbsKfqz zxgQo`g!%zg;|ZYg2V-O-A6;Y&sP0HOb$hkXmQbRXJkz_wt1IB1ZAqb^6Q+D5nG<2O zBRR})6RMh+tc=XFkz8iPxfzaXc?VOzrU(xXbi6;gy3(#V4SKkM{<#|~8BhJ4**{1GF<=m7zSR@dAoVa9_Y;*^~vIn9cnGC!) z@A3{~iBd(amylyZjHY9dNzkzxLUu8w(21wMo9x77=?~$hNJ-C%)J{!<=;HeCGtpJB zqy<&hC;bNpo&{&x`>Zo@*f8P`{}`&P^-MrIor}zQkZj z3olMB=7L^roQhTx7%qDKLvUaF1MK487m3zw4XMO^sh*#f04S>nKPPXoU(cdtV3ZjZ zW^)UM1#f)`@dvN+TB2F@MJakB92&Uxf57vR26V}~D3kg%Ea{^hFR#Ed48-x;ci*LX z-wG{vu*)vpL6^3s+$t*V?I;#^|0G{*(RG?o)f9;q)FTu0y1#iQM6%gMk!HJfY(U?= zpY{44SO!DA3Ke=Joqv&bAL}un+q^EiwEmaO^q;V#jGAz0vfdyA+!ucecjI!e#g=g> zk_kE19SN$L4OKloS*bUI@znnNE{8Ugm@I>$wZ5yoeSKOIGNVTJn&>8RWnCmbm=WD4 zAe*R$<1(}VCCufvK9kH)jJA6RwylKw%CEijWRsPs8jthx5XcCbQIkea$T86iz2<(z zmwzo;v1l0rUJFY6VW`sIfyhVzl$DokjdsQEk~{6QuoQ<5^ZAH~O+mW%n_-fST#$1~vYW%C=k_(S!@uU0@N&ocWK7@g_8B7|_y0wLP3sQ z>-Ig_i6jNY?mmH4Z#}P+pCPKeDb6v?MH8mMXe`-m)I5$!1f#ockN2@;fmlNGk0MQ( zCx;As(Uz!^2Yab^IWbOyWgpb|S;<9`E~eN2=4Vqsf85MuvQY-;H2e&3qd-q|cLPqV zu;&iZ)$Z?c5-EC#2WHrb>2&FuU2vTxV!K(9!YODJ;bCcTXg&hb_6YA^=Q8K*7Db*v5(P4`o_ykIppypGNr4*#9*dCK_#CVVi0c z9H!wX-;hqx&tPP&HnnuYmY1OTSFr5HsQ+e|M%ozre_I-jE=^CuvdjjX>U8`JmVDt) zWwoPIj}H$)R*Takn!hUH$X&2_n1-Pl+5g4^pGK$K;S?HK`@i65i+8F;GIs za?GS$uNi0tq_r!H<4@HejZE-5>e|ja1%rhBo{ubV{a<8}tP6mH7OoNl7MG*Q$#nNO z_P^eex?N6V4g$(EOaIl)!wMl=f<PvO_Wv#TfNug|^uo5)^24u@heg>lApZEb(Rce`417kSiZ1%6MVqe0-jPX`$3d?juG$01>zSdu7>z;b?kv0 z?zWfVzVyrFgyD*MsJeH{!aW)^;V>xEDCF&)exCiGMgfq?{U6ncOmMCJUkI2|iw7gy zXZ!+afgkuP07x8y(WQ-FgXSZcFcZbh-U8tj5eT7*~O)^gJUFIMI``2@pQ{8*mQ01ZgAo zfAlA_tqG9dbZGF!6$@Jb1Rn`xOift{-z|=eyG5ee&JTkj7l`9sY1sd!(dgmG1aH28 zUHtjZAIi7S{0n?05CLF{7JB;l>r9kJg1coX$Ta^nt->>CnrZF-{6@6p*CMgbFZikY z@9|#25ff8DGnUG`W|p~P*#j{dDr&?LI0-cVkmPItlX++=5Qp*$xP6E}Fc0qPdtk{w z#OoIgv-7_2!i~!j0D%gChnf&NZfyNEqP{MYr|DP8ScX78%1AZTsEHyF4owP#@w0&P zTF@Sug513svEQC4&PY3GBOfuNz02meUY}``&vXeqG{?)vR7d+te*{)@<=yS5Tt6u_ z0zou{u?K=0j)x-xLAS3){BIfbkGA*^(d`jk_HQP7hJeUPY`*KpX|>@{-(8wHvv#?b z%rLVBVbYCl#dco7UQXBZ@m}}LQCDvXbntG@l5_f&)E|{#^;aS;@KH7v%P=9lEoX3~0(oegiCT&lCf??NznP20Z^)?)UQ)a1HzNMN4vVD-5kFs- z!)kH(kk;QIN90rh&|J6?F8j7ziL5p`uvqdPvAtuO>F4dy#0_*>?zG^%DA3uycx+V# zMgAhbkdt7s6ah@Qy9{&*&CgzYuT03B3ccr~5`K{}b zWAds*H@dWO23X(2pv;=-SuFX_t4Q>mRra}^>yQWJRU%Gge)AjTx*wK@zBE6uSaJ)k zj=MN7UlzY1FPn7qEuDC*%lgmqum%>($Y6sCxo4MNz9sdB4*`&uskkNcH$5r8+65w? z+F`L2J}nOMdHJ^Z4SAc3U$$V=!`kJmnJM)NnHEdF!Q7$1^GfEo=H(2wyzbLyQlz6x zYflF|zmuoqV6hYw)WS+HD4*B*N8~95(Wo=Nxc;OB(H-)z78Xk(Kv?HE_;AVPEva{T z%>e~bjvHRsy1b9wb0s|JPis_*C7Uo_PiTF1zAt`5LAC1cTU!5a7q|mV(_yjXA!4?^ zK=H(f%NDh*K><)u4LOV>g%WfYfuFSvMlJaU5#+w4a|&hg8w#!?PvokW*AY)#z{GzR zK@f+UP$dl?h`LZ0@*V#yioN~rG}Wn4bD{PIoB zC@>1PCm&tawk4vY{|QAmFk=TSmY`vDix=&($c~xU1lh+gn-u)KK3;}!JW=-v*HvGZ zzvduVEP=r5q1C4&3!DEQR0m%_7%>AolY)cFF#${x~)`Kd{R01 zFL~MnizOSdH@b110{uU6_AMGIi{CIJ_eoPj(0fzO@uZwvgyPE%ZMf>tK<>% z@ik?OS_2u^6(l2CQQmb!)!1;j@@kRXmoe=80ZZC!mIZQaob`7{E!?~nBajhqDf8`c zVr`8dkcZ!7)`xmCSS*$y;e^~LkF&-X|ME!7lXLHYixEoExXOB$)J|hKH%iFngJL6C z7olY^vunW>fIh)Htw1o9VjA-S{3eIQh$^<2yc{!C&E&c z$WDJZvwLOPf-P%NgcQ9k?1c;$H(V$;!u9fW5-df5?D#K|qD5u%oBxI)#yZ2^qV47h zmDO(Ozck@;xh%H-;0VPe^bqk+BHLT9rzSg@+6sU@;HFbU@$s)8=7JVL^G8HK90kkB zfY8R8e-(F}A68wlX?3wU`Synu00Y2_4=n9forgOdny;bHmCIBKOF<$#{x$`^P1gIe zk{$1QgPI;L`dxodtN<7S{@K(>S7&s3h}7rh>0CJ~U?~V>d*3Lk{x6iHfA`XJ6JI~- zmKC;p)BsokkOH!cP}hz1?+SwCIBSGqTi5{FIuN+EV$}xpu*&C6QXird0`XS44$r&9 z4~97ONXhn1ui7--V6XxphnV_ntfD7NOWdbTLswV=#|nTvg0Gui-C5c-%W<4T73dgQEl11C^&%K5WS1=W zidPE;(dL;KN;d)!Vmhv?_gm zVg*0};t7WB6YIn^)~HmxhPA4fmnTC}g9wGmvMB4AsLBWiF(*vNqmX>iVfnPpV`Ey7 z*B6nP@XkG8S|i9N4Hw!=rqS|0S9Fd(S=0$d9FrZrQ(oUH|7x2EqD?Mnt;5);z%6pZ zmNK`cjTXYjKZ07fW$`>YbQY#Y;w(CCPA$7rUp^(qar+_^4;Eq`Kr+EB%Gaz1Z^t|ZQ5oRx5Z+ySS%LHNZ|hinn8H(01}1p00000 LNkvXXu0mjf6an;> literal 0 HcmV?d00001 diff --git a/resources/apps/bitcoind/0.21.1.2/instructions.md b/resources/apps/bitcoind/0.21.1.2/instructions.md new file mode 100644 index 0000000..358899e --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.2/instructions.md @@ -0,0 +1,23 @@ +# Bitcoin Core + +## Getting Started + +### Config + +Your node is highly configurable. Many settings are considered _advanced_ and should be used with caution. For the vast majority of users and use-cases, we recommend using the defaults. This is where you can change RPC credentials as well. Once configured, you may start your node! + +### Syncing + +Depending on your Internet bandwidth, your node should take approximately 5-7 days to sync from genesis to present. + +### Using a Wallet + +Enter your QuickConnect QR code **OR** your raw RPC credentials (both located in `Properties`) into any wallet that supports connecting to a remote node over Tor. For a full list of compatible wallets, see https://github.com/start9labs/bitcoind-wrapper/blob/master/docs/wallets.md. + +## Pruning + +Pruning is a process by which your node discards old blocks and transactions after it verifies them. Pruned nodes and archival nodes are both "full nodes" in that they are fully validating - they validate every block and transaction. Archival nodes store the entire blockchain and are useful to people interested in doing general or historical analysis, or being a provider of blockchain data to others (eg. a blockexplorer). + +The target of pruning on your Embassy is configurable and set by default to the minimum of 550MB (0.55 GB!), meaning the resulting blockchain will occupy a negligible amount of storage space. The maximum amount of blockchain data you can retain depends on the storage capacity your device. The config menu will not permit you to select a target that exceeds a certain percentage of your device's available capacity. + +For most use cases, we recommend sticking with a very low pruning setting. diff --git a/resources/apps/bitcoind/0.21.1.2/license.md b/resources/apps/bitcoind/0.21.1.2/license.md new file mode 100644 index 0000000..461bc73 --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.2/license.md @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2009-2020 The Bitcoin Core developers +Copyright (c) 2009-2020 Bitcoin Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/resources/apps/bitcoind/0.21.1.2/manifest.json b/resources/apps/bitcoind/0.21.1.2/manifest.json new file mode 100644 index 0000000..08fd6cb --- /dev/null +++ b/resources/apps/bitcoind/0.21.1.2/manifest.json @@ -0,0 +1,272 @@ +{ + "id": "bitcoind", + "title": "Bitcoin Core", + "version": "0.21.1.2", + "description": { + "short": "A Bitcoin Full Node by Bitcoin Core", + "long": "Bitcoin is an innovative payment network and a new kind of money. Bitcoin uses peer-to-peer technology to operate with no central authority or banks; managing transactions and the issuing of bitcoins is carried out collectively by the network. Bitcoin is open-source; its design is public, nobody owns or controls Bitcoin and everyone can take part. Through many of its unique properties, Bitcoin allows exciting uses that could not be covered by any previous payment system." + }, + "assets": { + "license": "LICENSE", + "instructions": "instructions.md", + "icon": "icon.png", + "docker-images": "image.tar", + "assets": null + }, + "build": [ + "make" + ], + "release-notes": "Upgrade to EmbassyOS v0.3.0", + "license": "mit", + "wrapper-repo": "https://github.com/Start9Labs/bitcoind-wrapper", + "upstream-repo": "https://github.com/bitcoin/bitcoin", + "support-site": "https://github.com/bitcoin/bitcoin/issues", + "marketing-site": "https://bitcoincore.org/", + "donation-url": null, + "alerts": { + "install": null, + "uninstall": "Uninstalling Bitcoin Core will result in permanent loss of data. Without a backup, any funds stored on your node's default hot wallet will be lost forever. If you are unsure, we recommend making a backup, just to be safe.", + "restore": "Restoring Bitcoin Core will overwrite its current data. You will lose any transactions recorded in watch-only wallets, and any funds you have received to the hot wallet, since the last backup.", + "start": null, + "stop": null + }, + "main": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "docker_entrypoint.sh", + "args": [], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "health-checks": { + "rpc": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "bitcoin-cli", + "args": [ + "-rpcconnect=bitcoind.embassy", + "-getinfo" + ], + "mounts": {}, + "io-format": "yaml", + "inject": true, + "shm-size-mb": null, + "critical": false + }, + "synced": { + "type": "docker", + "image": "utils", + "system": true, + "entrypoint": "/mnt/assets/check-synced.sh", + "args": [], + "mounts": { + "main": "/root/.bitcoin", + "utils": "/mnt/assets" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null, + "critical": false + } + }, + "config": { + "get": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "get", + "/root/.bitcoin", + "/mnt/assets/config_spec.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "set": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "set", + "bitcoind", + "/root/.bitcoin", + "/mnt/assets/config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + }, + "properties": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "properties", + "/root/.bitcoin" + ], + "mounts": { + "main": "/root/.bitcoin" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "volumes": { + "compat": { + "type": "assets" + }, + "main": { + "type": "data" + }, + "utils": { + "type": "assets" + } + }, + "min-os-version": "0.3.0", + "interfaces": { + "peer": { + "name": "Peer Interface", + "description": "Listens for incoming connections from peers on the bitcoin network", + "tor-config": { + "port-mapping": { + "8333": "8333" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "bitcoin" + ] + }, + "rpc": { + "name": "RPC Interface", + "description": "Listens for JSON-RPC commands", + "tor-config": { + "port-mapping": { + "8332": "8332" + } + }, + "lan-config": { + "8332": { + "ssl": false, + "mapping": 8332 + } + }, + "ui": false, + "protocols": [ + "tcp", + "http", + "json-rpc" + ] + }, + "zmq": { + "name": "ZeroMQ Interface", + "description": "Listens for subscriptions to the ZeroMQ raw block and raw transaction event streams", + "tor-config": { + "port-mapping": { + "28332": "28332", + "28333": "28333" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "zmq" + ] + } + }, + "backup": { + "create": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "create", + "/mnt/backup", + "/root/.bitcoin" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/.bitcoin" + }, + "io-format": null, + "inject": false, + "shm-size-mb": null + }, + "restore": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "restore", + "/mnt/backup", + "/root/.bitcoin" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/.bitcoin" + }, + "io-format": null, + "inject": false, + "shm-size-mb": null + } + }, + "migrations": { + "from": {}, + "to": {} + }, + "actions": { + "reindex": { + "name": "Reindex Blockchain", + "description": "Rebuilds the block and chainstate databases starting from genesis. If blocks already exist on disk, these are used rather than being redownloaded. However, since embassy bitcoin nodes are pruned by default, this usually means downloading the entire blockchain over again.", + "warning": "Blocks not stored on disk will be redownloaded in order to rebuild the database. If your node is pruned (embasssy nodes are pruned by default), this action is equivalent to syncing the node from scratch, so this process could take a couple of weeks.", + "implementation": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "reindex.sh", + "args": [], + "mounts": { + "main": "/root/.bitcoin" + }, + "io-format": "json", + "inject": false, + "shm-size-mb": null + }, + "allowed-statuses": [ + "running", + "stopped" + ], + "input-spec": {} + } + }, + "dependencies": {} +} \ No newline at end of file diff --git a/resources/apps/btc-rpc-proxy/0.3.2.1/hash.bin b/resources/apps/btc-rpc-proxy/0.3.2.1/hash.bin new file mode 100644 index 0000000..3a2531c --- /dev/null +++ b/resources/apps/btc-rpc-proxy/0.3.2.1/hash.bin @@ -0,0 +1 @@ +U4QIPEBA7F3J54XJK5X6A37SOFUY3RSUWZAL7UEYMNFPDSGQXNAJUB4CRE5C2CNS5JP4BHJEGKXNGVEYW3DN6K5HBOEX7SCK6IR2GHY diff --git a/resources/apps/btc-rpc-proxy/0.3.2.1/icon.png b/resources/apps/btc-rpc-proxy/0.3.2.1/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..88a7699e1d6c54d1ee47bbe2b45af53df7ef81e9 GIT binary patch literal 38948 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 literal 0 HcmV?d00001 diff --git a/resources/apps/btc-rpc-proxy/0.3.2.1/instructions.md b/resources/apps/btc-rpc-proxy/0.3.2.1/instructions.md new file mode 100644 index 0000000..675360d --- /dev/null +++ b/resources/apps/btc-rpc-proxy/0.3.2.1/instructions.md @@ -0,0 +1,17 @@ +# Usage Instructions + +## Dependencies + +Bitcoin Proxy requires a Bitcoin node to function. By default, Bitcoin Proxy will connect to the Bitcoin Core node running on your Embassy. + +## Config + +You must create at least one user to your Bitcoin Proxy config. Each user you create will receive their own RPC username and password, which can be used for remote access. This is also where you can edit RPC credentials. + +## Properties + +This section includes a convenient list of copyable properties, such as the quick connect URLs and RPC username and passwords for each configured user. + +## Using a Wallet + +Enter your user's QuickConnect QR code **OR** your raw user credentials (both located in `properties`) into any wallet that supports connecting to a remote node over Tor. For a full list of compatible wallets, see https://github.com/Start9Labs/btc-rpc-proxy-wrapper/blob/master/docs/wallets.md. \ No newline at end of file diff --git a/resources/apps/btc-rpc-proxy/0.3.2.1/license.md b/resources/apps/btc-rpc-proxy/0.3.2.1/license.md new file mode 100644 index 0000000..9286f55 --- /dev/null +++ b/resources/apps/btc-rpc-proxy/0.3.2.1/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Martin Habovštiak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/apps/btc-rpc-proxy/0.3.2.1/manifest.json b/resources/apps/btc-rpc-proxy/0.3.2.1/manifest.json new file mode 100644 index 0000000..cbf3010 --- /dev/null +++ b/resources/apps/btc-rpc-proxy/0.3.2.1/manifest.json @@ -0,0 +1,260 @@ +{ + "id": "btc-rpc-proxy", + "title": "Bitcoin Proxy", + "version": "0.3.2.1", + "description": { + "short": "Super charge your Bitcoin node", + "long": "Bitcoin Proxy enables you to specify several users and, for each user, the list of RPC calls they are allowed to make against your Bitcoin node. It also acts as a super charger for your pruned node. If a user or application requires a block not retained by your node, Bitcoin Proxy will dynamically fetch the block over the P2P network, then verify its hash to ensure validity. Your pruned node will now act like a full archival node!\n" + }, + "assets": { + "license": "LICENSE", + "instructions": "instructions.md", + "icon": "icon.png", + "docker-images": "image.tar", + "assets": null + }, + "build": [ + "make" + ], + "release-notes": "Upgrade to EmbassyOS v0.3.0", + "license": "mit", + "wrapper-repo": "https://github.com/Start9Labs/btc-rpc-proxy-wrapper", + "upstream-repo": "https://github.com/Kixunil/btc-rpc-proxy", + "support-site": "https://github.com/Kixunil/btc-rpc-proxy/issues", + "marketing-site": null, + "donation-url": null, + "alerts": { + "install": "After installing, you will need to manually enable the new RPC requests if you intend to use them. We strongly recommend enabling ALL requests for your primary user.\n", + "uninstall": null, + "restore": null, + "start": null, + "stop": null + }, + "main": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "docker_entrypoint.sh", + "args": [], + "mounts": { + "bitcoind": "/mnt/bitcoind", + "main": "/root" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "health-checks": { + "main": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "sh", + "args": [ + "-c", + "curl btc-rpc-proxy.embassy:8332 || exit 1" + ], + "mounts": {}, + "io-format": "yaml", + "inject": true, + "shm-size-mb": null, + "critical": true + } + }, + "config": { + "get": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "get", + "/root", + "/mnt/assets/config_spec.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "set": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "set", + "btc-rpc-proxy", + "/root", + "/mnt/assets/config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + }, + "properties": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "properties", + "/root" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "volumes": { + "bitcoind": { + "type": "pointer", + "package-id": "bitcoind", + "volume-id": "main", + "path": "/root/.bitcoin", + "readonly": true + }, + "compat": { + "type": "assets" + }, + "main": { + "type": "data" + } + }, + "min-os-version": "0.3.0", + "interfaces": { + "main": { + "name": "Network interface", + "description": "Specifies the interface to listen on for HTTP connections.", + "tor-config": { + "port-mapping": { + "8332": "8332" + } + }, + "lan-config": { + "8332": { + "ssl": true, + "mapping": 443 + } + }, + "ui": false, + "protocols": [ + "tcp", + "http" + ] + } + }, + "backup": { + "create": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "create", + "/mnt/backup", + "/root/data" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/data" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "restore": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "restore", + "/mnt/backup", + "/root/data" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/data" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + }, + "migrations": { + "from": {}, + "to": {} + }, + "actions": {}, + "dependencies": { + "bitcoind": { + "version": ">=0.21.1.2 <0.22.0", + "requirement": { + "type": "opt-out", + "how": "Can alternatively configure an external bitcoin node." + }, + "description": "Bitcoin Proxy sends RPC requests to a bitcoin node, like the Bitcoin Core service.", + "critical": false, + "config": { + "check": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "dependency", + "check", + "btc-rpc-proxy", + "bitcoind", + "/root", + "/mnt/assets/bitcoind_config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "auto-configure": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "dependency", + "auto-configure", + "btc-rpc-proxy", + "bitcoind", + "/root", + "/mnt/assets/bitcoind_config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + } + } + } +} \ No newline at end of file diff --git a/resources/apps/lnd/0.13.3.1/hash.bin b/resources/apps/lnd/0.13.3.1/hash.bin new file mode 100644 index 0000000..222152d --- /dev/null +++ b/resources/apps/lnd/0.13.3.1/hash.bin @@ -0,0 +1 @@ +PFLZO3TPFC57UTPBE3NKBLOTBRHSZH4DMNYQNX4FEQ54XCC6OB3R3UZEAS62ZLPNVRVNPRE2Z67O6NW4NHV2NEP5NPJ2FTDVHNJZFMY diff --git a/resources/apps/lnd/0.13.3.1/icon.png b/resources/apps/lnd/0.13.3.1/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ccd80873b008c43c10e41cd8f176b3beff64f233 GIT binary patch literal 4201 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 ZVrw#Wo&vA-?`{TwmARc+<%OFM{s)Fhi9P@T literal 0 HcmV?d00001 diff --git a/resources/apps/lnd/0.13.3.1/instructions.md b/resources/apps/lnd/0.13.3.1/instructions.md new file mode 100644 index 0000000..16c0e75 --- /dev/null +++ b/resources/apps/lnd/0.13.3.1/instructions.md @@ -0,0 +1,52 @@ +# Lightning Network Daemon (LND) + +## Dependencies + +LND on the Embassy requires a full archival Bitcoin node to function. Since your Embassy Bitcoin node is pruned by default, an additional service, Bitcoin Proxy, is also required. + +## LND Config + +Your LND node is highly configurable. Many settings are considered _advanced_ and should be used with caution. For the vast majority of users and use-cases, we recommend using the defaults. Once configured, you may start your node! + +## Bitcoin Proxy Config + +On the LND page, scroll down to find the Bitcoin Proxy dependency. Click `Configure`. This will automatically configure Bitcoin Proxy to satisfy LND. + +# Lightning Usage Guide + +## Using a Wallet + +Enter your LND-Connect QR code (located in `properties`) into any wallet that supports connecting to a remote LND node over Tor. For a list of compatible wallets, see https://github.com/start9labs/lnd-wrapper/blob/master/docs/wallets.md. + +## Getting On-Chain Funds + +Before you can open and channel and start transacting on the Lightning network, you need some Bitcoin stored on your LND node. Be advised, Bitcoin funds that you transfer to your LND node are hot, meaning, the are stored directly on your Embassy. There is no way to use cold storage when using Lightning, which is why people call it reckless. For this reason, it is usually unwise to move large amounts of Bitcoin to your LND node. That said, you don't want to move a tiny amount either, since that would limit your purchasing power on the Lightning network. We recommend moving about 500,000-5,000,000 satoshis, or .005-.05 Bitcoin, which at current (May 7, 2021) prices is about $250-$2,500. This gives you a solid amount of purchasing power, but hopefully wouldn't ruin your life if something were to go terribly wrong. If you feel comfortable using more Bitcoin, then by all means, go for it. + +## Depositing to LND + +When using LND or any wallet that is connected to LND it is important to note that until "Synced to Chain" in the Properties page is reporting ✅, your deposits to your LND on-chain wallet may not appear. + +## Opening a Channel and Getting Outbound Liquidity + +Once your LND node is synced, it's time to open a channel. Opening a channel with a well-connected node is how you get connected to the rest of the network, and it immediately grants you outbound liquidity. Meaning, you will be able to send money to others. Unless you are planning to become a Lightning Service Provider, you do not want to open more than a couple of channels at most. Managing many channels is difficult, it can be quite expensive, and unless you plan to devote significant resources in the form of time and Bitcoin, there is no profit in it. If your goal is to use Lightning to benefit from instant and near-free transactions, you only need 2-3 good channels. + +If you are looking for destinations for your first channel, we suggest you open a channel with the [Start9 HQ](025d28dc4c4f5ce4194c31c3109129cd741fafc1ff2f6ea53f97de2f58877b2295@64.225.19.231:9735) node, which is already very well connected. + +It is not recommended to open a channel less than 100,000 satoshi, or .001 BTC, or $50 USD in today's prices. Anything less, and it's possible that the cost to open and close the channel might approach the size of the channel itself. The bigger the channel you open, the more outbound liquidity you will have, which means you have more spending power on the network. In this tutorial, we are going to open a channel of 2,000,000 satoshi. When opening a channel with Start9 HQ, we ask that you make it a private channel, meaning it will not display publicly on network graph. The reason for this is that unless you plan to be a very active Lightning Node Operator, having public channels decreases not only the reliability of your node but also hurts Start9's ability to route payments for you. If you do intend to be a serious node operator, we require that your channel be for a minimum of 5,000,000 sats. Please contact us in one of our community channels for further details. + +## Getting Inbound Liquidity + +If you want to receive payments, you will need some inbound liquidity. + +The first, easiest, and best way to get inbound liquidity is to use your outbound liquidity to buy something. Any Bitcoin you spend using your outbound liquidity is Bitcoin you can now receive back. So if there is something you want to buy, like a Start9 Embassy or a t-shirt from the Start9 store, simply make the purchase, and you will then have inbound liquidity equal to the amount of Satoshis you spend. + +Option 2 is to personally ask Start9 for an invoice for however much inbound liquity you want. Then you send Bitcoin to the invoice, and in turn we will transfer fiat currency to you equal to the amount of the Bitcoin you send us. In other words, Start9 will buy some Bitcoin from you at market rate, such that you then have inbound liquidity. In either case, you are spending or selling some Bitcoin. + +The only way to get inbound liquidity without spending or selling Bitcoin is to convince someone to open a channel with you, just as you opened a channel with Start9 HQ. This may be a difficult task, since there is not much incentive for someone to open a channel with you unless you are also very well connected. Also, you will need to make sure that they too, are well connected with plenty of inbound liquidity, or else your inbound liquidity with them will not really matter. In other words, they might be the only person capable of paying you. + +So options 1 or 2 are best. Use your Lightning node's outbound liquidity to either purchase something or sell some Bitcoin. Now, you can pay and get paid using Lightning in an amount equal to your outbound and inbound liquidity. + +## Sending payments over Lightning + +Once you have open channels and are ready to transact on the Lightning Network, it is important to note that until "Synced to Graph" in the Properties page is reporting ✅, you may experience problems finding routes to your destination. + diff --git a/resources/apps/lnd/0.13.3.1/license.md b/resources/apps/lnd/0.13.3.1/license.md new file mode 100644 index 0000000..70f1f0d --- /dev/null +++ b/resources/apps/lnd/0.13.3.1/license.md @@ -0,0 +1,19 @@ +Copyright (C) 2015-2018 Lightning Labs and The Lightning Network Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/resources/apps/lnd/0.13.3.1/manifest.json b/resources/apps/lnd/0.13.3.1/manifest.json new file mode 100644 index 0000000..313f3b8 --- /dev/null +++ b/resources/apps/lnd/0.13.3.1/manifest.json @@ -0,0 +1,348 @@ +{ + "id": "lnd", + "title": "Lightning Network Daemon", + "version": "0.13.3.1", + "description": { + "short": "A complete implementation of a Lightning Network node by Lightning Labs", + "long": "LND fully conforms to the Lightning Network specification (BOLTs). BOLT stands for: Basis of Lightning Technology. In the current state lnd is capable of: creating channels, closing channels, managing all channel states (including the exceptional ones!), maintaining a fully authenticated+validated channel graph, performing path finding within the network, passively forwarding incoming payments, sending outgoing onion-encrypted payments through the network, updating advertised fee schedules, and automatic channel management (autopilot).\n" + }, + "assets": { + "license": "LICENSE", + "instructions": "instructions.md", + "icon": "icon.png", + "docker-images": "image.tar", + "assets": null + }, + "build": [ + "make" + ], + "release-notes": "Upgrade to EmbassyOS v0.3.0", + "license": "mit", + "wrapper-repo": "https://github.com/Start9Labs/lnd-wrapper", + "upstream-repo": "https://github.com/lightningnetwork/lnd", + "support-site": "https://lightning.engineering/slack.html", + "marketing-site": "https://lightning.engineering/", + "donation-url": null, + "alerts": { + "install": "READ CAREFULLY! LND and the Lightning Network are considered beta software. Please use with caution and do not risk more money than you are willing to lose. We encourage frequent backups. If for any reason, you need to restore LND from a backup, your on-chain wallet will be restored, but all your channels will be closed and their funds returned to your on-chain wallet, minus fees. It may also take some time for this process to occur.\n", + "uninstall": "READ CAREFULLY! Uninstalling LND will result in permanent loss of data, including its private keys for its on-chain wallet and all channel states. Please make a backup if you have any funds in your on-chain wallet or in any channels. Recovering from backup will restore your on-chain wallet, but due to the architecture of the Lightning Network, your channels cannot be recovered. All your channels will be closed and their funds returned to your on-chain wallet, minus fees. \n", + "restore": "Restoring LND will overwrite its current data, including its on-chain wallet and channels. Any channels opened since the last backup will be forgotten and may linger indefinitely, and channels contained in the backup will be closed and their funds returned to your on-chain wallet, minus fees.\n", + "start": null, + "stop": null + }, + "main": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "docker_entrypoint.sh", + "args": [], + "mounts": { + "bitcoind": "/mnt/bitcoind", + "btc-rpc-proxy": "/mnt/btc-rpc-proxy", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "health-checks": { + "synced": { + "type": "docker", + "image": "main", + "system": false, + "entrypoint": "health-check", + "args": [], + "mounts": {}, + "io-format": "yaml", + "inject": true, + "shm-size-mb": null, + "critical": false + } + }, + "config": { + "get": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "get", + "/root/.lnd", + "/mnt/assets/config_spec.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "set": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "config", + "set", + "lnd", + "/root/.lnd", + "/mnt/assets/config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + }, + "properties": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "properties", + "/root/.lnd" + ], + "mounts": { + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "volumes": { + "bitcoind": { + "type": "pointer", + "package-id": "bitcoind", + "volume-id": "main", + "path": "/root", + "readonly": true + }, + "btc-rpc-proxy": { + "type": "pointer", + "package-id": "btc-rpc-proxy", + "volume-id": "main", + "path": "/root", + "readonly": false + }, + "certificates": { + "type": "certificate", + "interface-id": "control" + }, + "compat": { + "type": "assets" + }, + "main": { + "type": "data" + } + }, + "min-os-version": "0.3.0", + "interfaces": { + "control": { + "name": "Control Interface", + "description": "Specifies the interfaces to listen on for RPC and REST connections.", + "tor-config": { + "port-mapping": { + "8080": "8080", + "10009": "10009" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "http", + "grpc" + ] + }, + "peer": { + "name": "Peer Interface", + "description": "Specifies the interfaces to listen on for p2p connections.", + "tor-config": { + "port-mapping": { + "9735": "9735" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "http" + ] + }, + "watchtower": { + "name": "Watchtower Interface", + "description": "Specifies the interfaces to listen on for watchtower client connections.", + "tor-config": { + "port-mapping": { + "9911": "9911" + } + }, + "lan-config": null, + "ui": false, + "protocols": [ + "tcp", + "grpc" + ] + } + }, + "backup": { + "create": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "duplicity", + "create", + "/mnt/backup", + "/root/.lnd" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "restore": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "/mnt/assets/backup-restore.sh", + "args": [ + "duplicity", + "restore", + "/mnt/backup", + "/root/.lnd" + ], + "mounts": { + "BACKUP": "/mnt/backup", + "compat": "/mnt/assets", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + }, + "migrations": { + "from": {}, + "to": {} + }, + "actions": {}, + "dependencies": { + "bitcoind": { + "version": ">=0.21.1.2 <0.22.0", + "requirement": { + "type": "opt-out", + "how": "Can alternatively configure an external bitcoin node." + }, + "description": "Used to subscribe to new block events.", + "critical": true, + "config": { + "check": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "dependency", + "check", + "lnd", + "bitcoind", + "/root/.lnd", + "/mnt/assets/bitcoind_config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.lnd" + }, + "io-format": null, + "inject": false, + "shm-size-mb": null + }, + "auto-configure": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "dependency", + "auto-configure", + "lnd", + "bitcoind", + "/root/.lnd", + "/mnt/assets/bitcoind_config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets" + }, + "io-format": null, + "inject": false, + "shm-size-mb": null + } + } + }, + "btc-rpc-proxy": { + "version": ">=0.3.2.1 <0.4.0", + "requirement": { + "type": "opt-out", + "how": "Can alternatively use the internal full archival bitcoind node or configure an external bitcoin node." + }, + "description": "Used to fetch validated blocks.", + "critical": false, + "config": { + "check": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "dependency", + "check", + "lnd", + "btc-rpc-proxy", + "/root/.lnd", + "/mnt/assets/btc-rpc-proxy_config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + }, + "auto-configure": { + "type": "docker", + "image": "compat", + "system": true, + "entrypoint": "compat", + "args": [ + "dependency", + "auto-configure", + "lnd", + "btc-rpc-proxy", + "/root/.lnd", + "/mnt/assets/btc-rpc-proxy_config_rules.yaml" + ], + "mounts": { + "compat": "/mnt/assets", + "main": "/root/.lnd" + }, + "io-format": "yaml", + "inject": false, + "shm-size-mb": null + } + } + } + } +} \ No newline at end of file From b8a84f540ad409979331dea9e24f896fa734a015 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Wed, 24 Nov 2021 18:19:39 -0700 Subject: [PATCH 08/12] add database model for ptracking package dependencies --- src/Model.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Model.hs b/src/Model.hs index 9f374b7..745def9 100644 --- a/src/Model.hs +++ b/src/Model.hs @@ -17,6 +17,7 @@ import Lib.Types.Category import Lib.Types.Emver import Orphans.Emver ( ) import Startlude +import Yesod.Persist ( PersistFilter(Eq) ) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| PkgRecord @@ -91,4 +92,13 @@ ErrorLogRecord message Text incidents Word32 UniqueLogRecord epoch commitHash sourceFile line target level message +PkgDependency + createdAt UTCTime + pkgId PkgRecordId + pkgVersion Version + depId PkgRecordId + depVersionRange VersionRange + UniquePkgVersion pkgId pkgVersion + deriving Eq + deriving Show |] From 1610c8c9fd547fa06da7842cd644fb0efb0e4e66 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Wed, 24 Nov 2021 18:20:32 -0700 Subject: [PATCH 09/12] reprganize database calls and marketplaces types --- src/Database/Marketplace.hs | 62 ++++++++++- src/Handler/Marketplace.hs | 180 ++----------------------------- src/Handler/Types/Marketplace.hs | 122 +++++++++++++++++++++ src/Lib/PkgRepository.hs | 4 +- test/Handler/AppSpec.hs | 16 +-- 5 files changed, 199 insertions(+), 185 deletions(-) create mode 100644 src/Handler/Types/Marketplace.hs diff --git a/src/Database/Marketplace.hs b/src/Database/Marketplace.hs index 5463a29..4e3d9d3 100644 --- a/src/Database/Marketplace.hs +++ b/src/Database/Marketplace.hs @@ -15,8 +15,6 @@ import Database.Esqueleto.Experimental , (&&.) , (++.) , (==.) - , Entity(entityKey, entityVal) - , SqlBackend , (^.) , desc , from @@ -31,12 +29,15 @@ import Database.Esqueleto.Experimental , valList , where_ , (||.) + , Value(unValue) ) import Database.Esqueleto.Experimental ( (:&)(..) , table ) -import Lib.Types.AppIndex ( PkgId ) +import Lib.Types.AppIndex ( VersionInfo(..) + , PkgId + ) import Lib.Types.Category import Lib.Types.Emver ( Version , VersionRange @@ -47,6 +48,14 @@ import Startlude hiding ( (%) , on , yield ) +import qualified Data.HashMap.Internal.Strict as HM +import Handler.Types.Marketplace ( ReleaseNotes(ReleaseNotes) ) +import qualified Database.Persist as P +import Database.Persist.Postgresql + hiding ( (||.) + , selectSource + , (==.) + ) searchServices :: (MonadResource m, MonadIO m) => Maybe CategoryTitle @@ -112,3 +121,50 @@ filterOsCompatible :: Monad m filterOsCompatible p = awaitForever $ \(app, versions, requestedVersion) -> do let compatible = filter (p . versionRecordOsVersion . entityVal) versions when (not $ null compatible) $ yield (app, compatible, requestedVersion) + + +fetchAllAppVersions :: MonadUnliftIO m => ConnectionPool -> PkgId -> m ([VersionInfo], ReleaseNotes) +fetchAllAppVersions appConnPool appId = do + entityAppVersions <- runSqlPool (P.selectList [VersionRecordPkgId P.==. PkgRecordKey appId] []) appConnPool + let vers = entityVal <$> entityAppVersions + let vv = mapSVersionToVersionInfo vers + let mappedVersions = ReleaseNotes $ HM.fromList $ (\v -> (versionInfoVersion v, versionInfoReleaseNotes v)) <$> vv + pure $ (sortOn (Down . versionInfoVersion) vv, mappedVersions) + where + mapSVersionToVersionInfo :: [VersionRecord] -> [VersionInfo] + mapSVersionToVersionInfo sv = do + (\v -> VersionInfo { versionInfoVersion = versionRecordNumber v + , versionInfoReleaseNotes = versionRecordReleaseNotes v + , versionInfoDependencies = HM.empty + , versionInfoOsVersion = versionRecordOsVersion v + , versionInfoInstallAlert = Nothing + } + ) + <$> sv + +fetchLatestApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe (P.Entity PkgRecord, P.Entity VersionRecord)) +fetchLatestApp appId = fmap headMay . sortResults . select $ do + (service :& version) <- + from + $ table @PkgRecord + `innerJoin` table @VersionRecord + `on` (\(service :& version) -> service ^. PkgRecordId ==. version ^. VersionRecordPkgId) + where_ (service ^. PkgRecordId ==. val (PkgRecordKey appId)) + pure (service, version) + where sortResults = fmap $ sortOn (Down . versionRecordNumber . entityVal . snd) + + +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 diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index 0da92bc..9e1be6d 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -42,8 +42,6 @@ import Data.Aeson ( (.:) , decode , eitherDecode , eitherDecodeStrict - , object - , withObject ) import qualified Data.Attoparsec.Text as Atto import Data.ByteArray.Encoding ( Base(Base16) @@ -61,28 +59,22 @@ import Data.String.Interpolate.IsString ( i ) import qualified Data.Text as T import Database.Esqueleto.Experimental - ( (:&)((:&)) - , (==.) - , Entity(entityKey, entityVal) + ( Entity(entityKey, entityVal) , SqlBackend - , Value(unValue) , (^.) , desc , from - , in_ - , innerJoin - , on , orderBy , select , table - , val - , valList - , where_ ) import Database.Marketplace ( filterOsCompatible , getPkgData , searchServices , zipVersions + , fetchAllAppVersions + , fetchLatestApp + , fetchAppCategories ) import qualified Database.Persist as P import Database.Persist ( PersistUniqueRead(getBy) @@ -98,7 +90,6 @@ import Lib.PkgRepository ( getManifest ) import Lib.Types.AppIndex ( PkgId(PkgId) , PackageDependency(packageDependencyVersion) , PackageManifest(packageManifestDependencies) - , VersionInfo(..) ) import Lib.Types.AppIndex ( ) import Lib.Types.Category ( CategoryTitle(..) ) @@ -114,7 +105,6 @@ import Model ( Category(..) , EosHash(EosHash, eosHashHash) , Key(PkgRecordKey, unPkgRecordKey) , OsVersion(..) - , PkgCategory , PkgRecord(..) , Unique(UniqueVersion) , VersionRecord(..) @@ -132,8 +122,6 @@ import UnliftIO.Async ( concurrently import UnliftIO.Directory ( listDirectory ) import Util.Shared ( getVersionSpecFromQuery ) import Yesod.Core ( MonadResource - , ToContent(..) - , ToTypedContent(..) , TypedContent , YesodRequest(..) , addHeader @@ -158,115 +146,7 @@ import Database.Persist.Postgresql ( ConnectionPool ) import Control.Monad.Reader.Has ( Has , ask ) - -type URL = Text -newtype CategoryRes = CategoryRes { - categories :: [CategoryTitle] -} deriving (Show, Generic) -instance ToJSON CategoryRes -instance ToContent CategoryRes where - toContent = toContent . toJSON -instance ToTypedContent CategoryRes where - toTypedContent = toTypedContent . toJSON -data PackageRes = PackageRes - { packageResIcon :: URL - , packageResManifest :: Data.Aeson.Value -- PackageManifest - , packageResCategories :: [CategoryTitle] - , packageResInstructions :: URL - , packageResLicense :: URL - , packageResVersions :: [Version] - , packageResDependencies :: HM.HashMap PkgId DependencyRes - } - deriving (Show, Generic) -newtype ReleaseNotes = ReleaseNotes { unReleaseNotes :: HM.HashMap Version Text } - deriving (Eq, Show) -instance ToJSON ReleaseNotes where - toJSON ReleaseNotes {..} = object [ t .= v | (k, v) <- HM.toList unReleaseNotes, let (String t) = toJSON k ] -instance ToContent ReleaseNotes where - toContent = toContent . toJSON -instance ToTypedContent ReleaseNotes where - toTypedContent = toTypedContent . toJSON -instance ToJSON PackageRes where - toJSON PackageRes {..} = object - [ "icon" .= packageResIcon - , "license" .= packageResLicense - , "instructions" .= packageResInstructions - , "manifest" .= packageResManifest - , "categories" .= packageResCategories - , "versions" .= packageResVersions - , "dependency-metadata" .= packageResDependencies - ] -instance FromJSON PackageRes where - parseJSON = withObject "PackageRes" $ \o -> do - packageResIcon <- o .: "icon" - packageResLicense <- o .: "license" - packageResInstructions <- o .: "instructions" - packageResManifest <- o .: "manifest" - packageResCategories <- o .: "categories" - packageResVersions <- o .: "versions" - packageResDependencies <- o .: "dependency-metadata" - pure PackageRes { .. } -data DependencyRes = DependencyRes - { dependencyResTitle :: PkgId - , dependencyResIcon :: URL - } - deriving (Eq, Show) -instance ToJSON DependencyRes where - toJSON DependencyRes {..} = object ["icon" .= dependencyResIcon, "title" .= dependencyResTitle] -instance FromJSON DependencyRes where - parseJSON = withObject "DependencyRes" $ \o -> do - dependencyResIcon <- o .: "icon" - dependencyResTitle <- o .: "title" - pure DependencyRes { .. } -newtype PackageListRes = PackageListRes [PackageRes] - deriving (Generic) -instance ToJSON PackageListRes -instance ToContent PackageListRes where - toContent = toContent . toJSON -instance ToTypedContent PackageListRes where - toTypedContent = toTypedContent . toJSON - -newtype VersionLatestRes = VersionLatestRes (HM.HashMap PkgId (Maybe Version)) - deriving (Show, Generic) -instance ToJSON VersionLatestRes -instance ToContent VersionLatestRes where - toContent = toContent . toJSON -instance ToTypedContent VersionLatestRes where - toTypedContent = toTypedContent . toJSON -data OrderArrangement = ASC | DESC - deriving (Eq, Show, Read) -data PackageListDefaults = PackageListDefaults - { packageListOrder :: OrderArrangement - , packageListPageLimit :: Int -- the number of items per page - , packageListPageNumber :: Int -- the page you are on - , packageListCategory :: Maybe CategoryTitle - , packageListQuery :: Text - } - deriving (Eq, Show, Read) -data EosRes = EosRes - { eosResVersion :: Version - , eosResHeadline :: Text - , eosResReleaseNotes :: ReleaseNotes - } - deriving (Eq, Show, Generic) -instance ToJSON EosRes where - toJSON EosRes {..} = - object ["version" .= eosResVersion, "headline" .= eosResHeadline, "release-notes" .= eosResReleaseNotes] -instance ToContent EosRes where - toContent = toContent . toJSON -instance ToTypedContent EosRes where - toTypedContent = toTypedContent . toJSON - -data PackageReq = PackageReq - { packageReqId :: PkgId - , packageReqVersion :: VersionRange - } - deriving Show -instance FromJSON PackageReq where - parseJSON = withObject "package version" $ \o -> do - packageReqId <- o .: "id" - packageReqVersion <- o .: "version" - pure PackageReq { .. } +import Handler.Types.Marketplace getCategoriesR :: Handler CategoryRes getCategoriesR = do @@ -301,7 +181,8 @@ getReleaseNotesR = do case lookup "id" getParameters of Nothing -> sendResponseStatus status400 (InvalidParamsE "get:id" "") Just package -> do - (_, notes) <- fetchAllAppVersions (PkgId package) + appConnPool <- appConnPool <$> getYesod + (_, notes) <- runDB $ fetchAllAppVersions appConnPool (PkgId package) pure notes getEosR :: Handler TypedContent @@ -562,50 +443,3 @@ mapDependencyMetadata domain metadata (appId, depInfo) = do , dependencyResIcon = [i|https://#{domain}/package/icon/#{appId}?spec==#{version}|] } ) - -fetchAllAppVersions :: PkgId -> Handler ([VersionInfo], ReleaseNotes) -fetchAllAppVersions appId = do - entityAppVersions <- runDB $ P.selectList [VersionRecordPkgId P.==. PkgRecordKey appId] [] - let vers = entityVal <$> entityAppVersions - let vv = mapSVersionToVersionInfo vers - let mappedVersions = ReleaseNotes $ HM.fromList $ (\v -> (versionInfoVersion v, versionInfoReleaseNotes v)) <$> vv - pure (sortOn (Down . versionInfoVersion) vv, mappedVersions) - where - mapSVersionToVersionInfo :: [VersionRecord] -> [VersionInfo] - mapSVersionToVersionInfo sv = do - (\v -> VersionInfo { versionInfoVersion = versionRecordNumber v - , versionInfoReleaseNotes = versionRecordReleaseNotes v - , versionInfoDependencies = HM.empty - , versionInfoOsVersion = versionRecordOsVersion v - , versionInfoInstallAlert = Nothing - } - ) - <$> sv - - -fetchLatestApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe (P.Entity PkgRecord, P.Entity VersionRecord)) -fetchLatestApp appId = fmap headMay . sortResults . select $ do - (service :& version) <- - from - $ table @PkgRecord - `innerJoin` table @VersionRecord - `on` (\(service :& version) -> service ^. PkgRecordId ==. version ^. VersionRecordPkgId) - where_ (service ^. PkgRecordId ==. val (PkgRecordKey appId)) - pure (service, version) - where sortResults = fmap $ sortOn (Down . versionRecordNumber . entityVal . snd) - - -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 diff --git a/src/Handler/Types/Marketplace.hs b/src/Handler/Types/Marketplace.hs new file mode 100644 index 0000000..8aad326 --- /dev/null +++ b/src/Handler/Types/Marketplace.hs @@ -0,0 +1,122 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE DeriveGeneric #-} +module Handler.Types.Marketplace where +import Lib.Types.Category ( CategoryTitle ) +import Data.Aeson +import Startlude +import Yesod +import qualified Data.HashMap.Internal.Strict as HM +import Lib.Types.Emver ( VersionRange + , Version + ) +import Lib.Types.AppIndex ( PkgId ) + + +type URL = Text +newtype CategoryRes = CategoryRes { + categories :: [CategoryTitle] +} deriving (Show, Generic) +instance ToJSON CategoryRes +instance ToContent CategoryRes where + toContent = toContent . toJSON +instance ToTypedContent CategoryRes where + toTypedContent = toTypedContent . toJSON +data PackageRes = PackageRes + { packageResIcon :: URL + , packageResManifest :: Data.Aeson.Value -- PackageManifest + , packageResCategories :: [CategoryTitle] + , packageResInstructions :: URL + , packageResLicense :: URL + , packageResVersions :: [Version] + , packageResDependencies :: HM.HashMap PkgId DependencyRes + } + deriving (Show, Generic) +newtype ReleaseNotes = ReleaseNotes { unReleaseNotes :: HM.HashMap Version Text } + deriving (Eq, Show) +instance ToJSON ReleaseNotes where + toJSON ReleaseNotes {..} = object [ t .= v | (k, v) <- HM.toList unReleaseNotes, let (String t) = toJSON k ] +instance ToContent ReleaseNotes where + toContent = toContent . toJSON +instance ToTypedContent ReleaseNotes where + toTypedContent = toTypedContent . toJSON +instance ToJSON PackageRes where + toJSON PackageRes {..} = object + [ "icon" .= packageResIcon + , "license" .= packageResLicense + , "instructions" .= packageResInstructions + , "manifest" .= packageResManifest + , "categories" .= packageResCategories + , "versions" .= packageResVersions + , "dependency-metadata" .= packageResDependencies + ] +instance FromJSON PackageRes where + parseJSON = withObject "PackageRes" $ \o -> do + packageResIcon <- o .: "icon" + packageResLicense <- o .: "license" + packageResInstructions <- o .: "instructions" + packageResManifest <- o .: "manifest" + packageResCategories <- o .: "categories" + packageResVersions <- o .: "versions" + packageResDependencies <- o .: "dependency-metadata" + pure PackageRes { .. } +data DependencyRes = DependencyRes + { dependencyResTitle :: PkgId + , dependencyResIcon :: URL + } + deriving (Eq, Show) +instance ToJSON DependencyRes where + toJSON DependencyRes {..} = object ["icon" .= dependencyResIcon, "title" .= dependencyResTitle] +instance FromJSON DependencyRes where + parseJSON = withObject "DependencyRes" $ \o -> do + dependencyResIcon <- o .: "icon" + dependencyResTitle <- o .: "title" + pure DependencyRes { .. } +newtype PackageListRes = PackageListRes [PackageRes] + deriving (Generic) +instance ToJSON PackageListRes +instance ToContent PackageListRes where + toContent = toContent . toJSON +instance ToTypedContent PackageListRes where + toTypedContent = toTypedContent . toJSON + +newtype VersionLatestRes = VersionLatestRes (HM.HashMap PkgId (Maybe Version)) + deriving (Show, Generic) +instance ToJSON VersionLatestRes +instance ToContent VersionLatestRes where + toContent = toContent . toJSON +instance ToTypedContent VersionLatestRes where + toTypedContent = toTypedContent . toJSON +data OrderArrangement = ASC | DESC + deriving (Eq, Show, Read) +data PackageListDefaults = PackageListDefaults + { packageListOrder :: OrderArrangement + , packageListPageLimit :: Int -- the number of items per page + , packageListPageNumber :: Int -- the page you are on + , packageListCategory :: Maybe CategoryTitle + , packageListQuery :: Text + } + deriving (Eq, Show, Read) +data EosRes = EosRes + { eosResVersion :: Version + , eosResHeadline :: Text + , eosResReleaseNotes :: ReleaseNotes + } + deriving (Eq, Show, Generic) +instance ToJSON EosRes where + toJSON EosRes {..} = + object ["version" .= eosResVersion, "headline" .= eosResHeadline, "release-notes" .= eosResReleaseNotes] +instance ToContent EosRes where + toContent = toContent . toJSON +instance ToTypedContent EosRes where + toTypedContent = toTypedContent . toJSON + +data PackageReq = PackageReq + { packageReqId :: PkgId + , packageReqVersion :: VersionRange + } + deriving Show +instance FromJSON PackageReq where + parseJSON = withObject "package version" $ \o -> do + packageReqId <- o .: "id" + packageReqVersion <- o .: "version" + pure PackageReq { .. } diff --git a/src/Lib/PkgRepository.hs b/src/Lib/PkgRepository.hs index e826228..185a127 100644 --- a/src/Lib/PkgRepository.hs +++ b/src/Lib/PkgRepository.hs @@ -143,6 +143,7 @@ getBestVersion :: (MonadIO m, MonadReader r m, Has PkgRepo r, MonadLogger m) -> m (Maybe Version) getBestVersion pkg spec = headMay . sortOn Down <$> getViableVersions pkg spec +-- TODO add loadDependencies -- extract all package assets into their own respective files extractPkg :: (MonadUnliftIO m, MonadReader r m, Has PkgRepo r, MonadLoggerIO m) => FilePath -> m () extractPkg fp = handle @_ @SomeException cleanup $ do @@ -193,7 +194,8 @@ watchPkgRepoRoot = do stop <- watchTree watchManager root onlyAdded $ \evt -> do let pkg = eventPath evt -- TODO: validate that package path is an actual s9pk and is in a correctly conforming path. - void . forkIO $ runInIO (extractPkg pkg) + void . forkIO $ runInIO $ do + (extractPkg pkg) takeMVar box stop pure $ tryPutMVar box () diff --git a/test/Handler/AppSpec.hs b/test/Handler/AppSpec.hs index b3431d6..b798a09 100644 --- a/test/Handler/AppSpec.hs +++ b/test/Handler/AppSpec.hs @@ -17,7 +17,7 @@ import Seed import Lib.Types.AppIndex import Data.Aeson import Data.Either.Extra -import Handler.Marketplace ( PackageListRes ) +import Handler.Marketplace ( PackageRes ) spec :: Spec spec = do @@ -27,7 +27,7 @@ spec = do setMethod "GET" setUrl ("/package/index" :: Text) statusIs 200 - (res :: PackageListRes) <- requireJSONResponse + (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have two packages" (length res) 3 describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version" $ do _ <- seedBitcoinLndStack @@ -35,7 +35,7 @@ spec = do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\"=0.21.1.2\"}]" :: Text) statusIs 200 - (res :: PackageListRes) <- requireJSONResponse + (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg @@ -49,17 +49,17 @@ spec = do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"lnd\",\"version\":\"=0.13.3.1\"}]" :: Text) statusIs 200 - (res :: PackageListRes) <- requireJSONResponse + (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res - assertEq "package dependency metadata should not be empty" (null $ packageResDependencyInfo pkg) False + assertEq "package dependency metadata should not be empty" (null $ packageResDependencies pkg) False describe "GET /package/index?ids" $ withApp $ it "returns list of packages at exactly specified version" $ do _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\"=0.21.1.1\"}]" :: Text) statusIs 200 - (res :: PackageListRes) <- requireJSONResponse + (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg @@ -70,7 +70,7 @@ spec = do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\">=0.21.1.1\"}]" :: Text) statusIs 200 - (res :: PackageListRes) <- requireJSONResponse + (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg @@ -81,7 +81,7 @@ spec = do setMethod "GET" setUrl ("/package/index?ids=[{\"id\":\"bitcoind\",\"version\":\">=0.21.1.2\"}]" :: Text) statusIs 200 - (res :: PackageListRes) <- requireJSONResponse + (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 let pkg = fromJust $ head res let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg From 64d432f2c92c6b591036c0cbb97a29f444319084 Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Thu, 2 Dec 2021 08:06:47 -0700 Subject: [PATCH 10/12] organization refactor separating database actions, data transformations, and api type constructs into separate components --- src/Application.hs | 3 +- src/Database/Marketplace.hs | 127 +++++++++--------- src/Foundation.hs | 4 +- src/Handler/Marketplace.hs | 222 +++++++++---------------------- src/Handler/Types/Marketplace.hs | 27 ++-- src/Lib/Error.hs | 6 - src/Lib/PkgRepository.hs | 54 ++++++-- src/Lib/Types/AppIndex.hs | 2 - src/Model.hs | 4 +- src/Util/Shared.hs | 77 ++++++++++- stack.yaml | 1 + test/Handler/AppSpec.hs | 22 ++- test/Seed.hs | 38 ++++-- 13 files changed, 304 insertions(+), 283 deletions(-) diff --git a/src/Application.hs b/src/Application.hs index ffa7c46..cd52fcc 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -132,13 +132,14 @@ makeFoundation appSettings = do mkFoundation (panic "connPool forced in tempFoundation") (panic "stopFsNotify forced in tempFoundation") logFunc = messageLoggerSource tempFoundation appLogger - stop <- runLoggingT (runReaderT watchPkgRepoRoot appSettings) logFunc createDirectoryIfMissing True (errorLogRoot appSettings) -- Create the database connection pool pool <- flip runLoggingT logFunc $ createPostgresqlPool (pgConnStr $ appDatabaseConf appSettings) (pgPoolSize . appDatabaseConf $ appSettings) + stop <- runLoggingT (runReaderT (watchPkgRepoRoot pool) appSettings) logFunc + -- Preform database migration using application logging settings runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc diff --git a/src/Database/Marketplace.hs b/src/Database/Marketplace.hs index 4e3d9d3..a87ea80 100644 --- a/src/Database/Marketplace.hs +++ b/src/Database/Marketplace.hs @@ -14,6 +14,7 @@ import Database.Esqueleto.Experimental ( (%) , (&&.) , (++.) + , (:&)(..) , (==.) , (^.) , desc @@ -25,37 +26,28 @@ import Database.Esqueleto.Experimental , orderBy , select , selectSource + , table , val , valList , where_ , (||.) - , Value(unValue) ) -import Database.Esqueleto.Experimental - ( (:&)(..) - , table - ) -import Lib.Types.AppIndex ( VersionInfo(..) - , PkgId +import qualified Database.Persist as P +import Database.Persist.Postgresql + hiding ( (==.) + , getJust + , selectSource + , (||.) ) +import Lib.Types.AppIndex ( PkgId ) import Lib.Types.Category -import Lib.Types.Emver ( Version - , VersionRange - ) +import Lib.Types.Emver ( Version ) import Model import Startlude hiding ( (%) , from , on , yield ) -import qualified Data.HashMap.Internal.Strict as HM -import Handler.Types.Marketplace ( ReleaseNotes(ReleaseNotes) ) -import qualified Database.Persist as P -import Database.Persist.Postgresql - hiding ( (||.) - , selectSource - , (==.) - ) searchServices :: (MonadResource m, MonadIO m) => Maybe CategoryTitle @@ -101,46 +93,69 @@ getPkgData pkgs = selectSource $ do where_ (pkgData ^. PkgRecordId `in_` valList (PkgRecordKey <$> pkgs)) pure pkgData +getPkgDependencyData :: MonadIO m + => Key PkgRecord + -> Version + -> ReaderT SqlBackend m ([(Entity PkgDependency, Entity PkgRecord)]) +getPkgDependencyData pkgId pkgVersion = select $ do + pd <- from + (do + (pkgDepRecord :& depPkgRecord) <- + from + $ table @PkgDependency + `innerJoin` table @PkgRecord + `on` (\(pdr :& dpr) -> dpr ^. PkgRecordId ==. pdr ^. PkgDependencyDepId) + where_ (pkgDepRecord ^. PkgDependencyPkgId ==. (val pkgId)) + where_ (pkgDepRecord ^. PkgDependencyPkgVersion ==. val pkgVersion) + pure (pkgDepRecord, depPkgRecord) + ) + pure pd + +zipCategories :: MonadUnliftIO m + => ConduitT + (Entity PkgRecord, [Entity VersionRecord]) + (Entity PkgRecord, [Entity VersionRecord], [Entity Category]) + (ReaderT SqlBackend m) + () +zipCategories = awaitForever $ \(pkg, vers) -> do + let pkgDbId = entityKey pkg + raw <- lift $ select $ do + (sc :& cat) <- + from + $ table @PkgCategory + `innerJoin` table @Category + `on` (\(sc :& cat) -> sc ^. PkgCategoryCategoryId ==. cat ^. CategoryId) + where_ (sc ^. PkgCategoryPkgId ==. val pkgDbId) + pure cat + yield (pkg, vers, raw) + zipVersions :: MonadUnliftIO m => ConduitT (Entity PkgRecord) (Entity PkgRecord, [Entity VersionRecord]) (ReaderT SqlBackend m) () -zipVersions = awaitForever $ \i -> do - let appDbId = entityKey i +zipVersions = awaitForever $ \pkg -> do + let appDbId = entityKey pkg res <- lift $ select $ do v <- from $ table @VersionRecord where_ $ v ^. VersionRecordPkgId ==. val appDbId + -- first value in list will be latest version + orderBy [desc (v ^. VersionRecordNumber)] pure v - yield (i, res) + yield (pkg, res) -filterOsCompatible :: Monad m - => (Version -> Bool) - -> ConduitT - (Entity PkgRecord, [Entity VersionRecord], VersionRange) - (Entity PkgRecord, [Entity VersionRecord], VersionRange) - m - () -filterOsCompatible p = awaitForever $ \(app, versions, requestedVersion) -> do - let compatible = filter (p . versionRecordOsVersion . entityVal) versions - when (not $ null compatible) $ yield (app, compatible, requestedVersion) +zipDependencyVersions :: (Monad m, MonadIO m) + => (Entity PkgDependency, Entity PkgRecord) + -> ReaderT SqlBackend m (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) +zipDependencyVersions (pkgDepRecord, depRecord) = do + let pkgDbId = entityKey $ depRecord + depVers <- select $ do + v <- from $ table @VersionRecord + where_ $ v ^. VersionRecordPkgId ==. val pkgDbId + pure v + pure $ (pkgDepRecord, depRecord, depVers) - -fetchAllAppVersions :: MonadUnliftIO m => ConnectionPool -> PkgId -> m ([VersionInfo], ReleaseNotes) +fetchAllAppVersions :: MonadUnliftIO m => ConnectionPool -> PkgId -> m [VersionRecord] fetchAllAppVersions appConnPool appId = do entityAppVersions <- runSqlPool (P.selectList [VersionRecordPkgId P.==. PkgRecordKey appId] []) appConnPool - let vers = entityVal <$> entityAppVersions - let vv = mapSVersionToVersionInfo vers - let mappedVersions = ReleaseNotes $ HM.fromList $ (\v -> (versionInfoVersion v, versionInfoReleaseNotes v)) <$> vv - pure $ (sortOn (Down . versionInfoVersion) vv, mappedVersions) - where - mapSVersionToVersionInfo :: [VersionRecord] -> [VersionInfo] - mapSVersionToVersionInfo sv = do - (\v -> VersionInfo { versionInfoVersion = versionRecordNumber v - , versionInfoReleaseNotes = versionRecordReleaseNotes v - , versionInfoDependencies = HM.empty - , versionInfoOsVersion = versionRecordOsVersion v - , versionInfoInstallAlert = Nothing - } - ) - <$> sv + pure $ entityVal <$> entityAppVersions fetchLatestApp :: MonadIO m => PkgId -> ReaderT SqlBackend m (Maybe (P.Entity PkgRecord, P.Entity VersionRecord)) fetchLatestApp appId = fmap headMay . sortResults . select $ do @@ -152,19 +167,3 @@ fetchLatestApp appId = fmap headMay . sortResults . select $ do where_ (service ^. PkgRecordId ==. val (PkgRecordKey appId)) pure (service, version) where sortResults = fmap $ sortOn (Down . versionRecordNumber . entityVal . snd) - - -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 diff --git a/src/Foundation.hs b/src/Foundation.hs index 44e1393..4d4db48 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -7,6 +7,7 @@ {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} + module Foundation where import Startlude hiding ( Handler ) @@ -75,9 +76,6 @@ instance Has AppSettings RegistryCtx where extract = appSettings update f ctx = ctx { appSettings = f (appSettings ctx) } - - - setWebProcessThreadId :: (ThreadId, ThreadId) -> RegistryCtx -> IO () setWebProcessThreadId tid a = putMVar (appWebServerThreadId a) $ tid diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index 9e1be6d..96da839 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -25,21 +25,11 @@ import Conduit ( (.|) , sinkList , sourceFile , takeC - , MonadUnliftIO - ) -import Control.Monad.Except.CoHas ( liftEither ) - -import Control.Parallel.Strategies ( parMap - , rpar + ) import Crypto.Hash ( SHA256 ) import Crypto.Hash.Conduit ( hashFile ) -import Data.Aeson ( (.:) - , FromJSON(parseJSON) - , KeyValue((.=)) - , ToJSON(toJSON) - , Value(String) - , decode +import Data.Aeson ( decode , eitherDecode , eitherDecodeStrict ) @@ -54,7 +44,6 @@ import Data.List ( head , lookup , sortOn ) -import Data.Semigroup ( Max(Max, getMax) ) import Data.String.Interpolate.IsString ( i ) import qualified Data.Text as T @@ -68,15 +57,13 @@ import Database.Esqueleto.Experimental , select , table ) -import Database.Marketplace ( filterOsCompatible - , getPkgData +import Database.Marketplace ( getPkgData , searchServices , zipVersions , fetchAllAppVersions , fetchLatestApp - , fetchAppCategories + , getPkgDependencyData, zipDependencyVersions, zipCategories ) -import qualified Database.Persist as P import Database.Persist ( PersistUniqueRead(getBy) , insertUnique ) @@ -84,17 +71,16 @@ import Foundation ( Handler , RegistryCtx(appSettings, appConnPool) ) import Lib.Error ( S9Error(..) - , toStatus + ) import Lib.PkgRepository ( getManifest ) import Lib.Types.AppIndex ( PkgId(PkgId) - , PackageDependency(packageDependencyVersion) - , PackageManifest(packageManifestDependencies) + + ) import Lib.Types.AppIndex ( ) import Lib.Types.Category ( CategoryTitle(..) ) -import Lib.Types.Emver ( (<||) - , Version +import Lib.Types.Emver ( Version , VersionRange(Any) , parseRange , parseVersion @@ -103,7 +89,7 @@ import Lib.Types.Emver ( (<||) import Model ( Category(..) , EntityField(..) , EosHash(EosHash, eosHashHash) - , Key(PkgRecordKey, unPkgRecordKey) + , Key(unPkgRecordKey) , OsVersion(..) , PkgRecord(..) , Unique(UniqueVersion) @@ -120,7 +106,7 @@ import UnliftIO.Async ( concurrently , mapConcurrently ) import UnliftIO.Directory ( listDirectory ) -import Util.Shared ( getVersionSpecFromQuery ) +import Util.Shared ( getVersionSpecFromQuery, filterLatestVersionFromSpec, filterPkgOsCompatible, filterDependencyOsCompatible, filterDependencyBestVersion ) import Yesod.Core ( MonadResource , TypedContent , YesodRequest(..) @@ -136,13 +122,7 @@ import Yesod.Core ( MonadResource ) import Yesod.Persist ( YesodDB ) import Yesod.Persist.Core ( YesodPersist(runDB) ) -import Data.Tuple.Extra hiding ( second - , first - , (&&&) - ) import Control.Monad.Logger -import Database.Persist.Sql ( runSqlPool ) -import Database.Persist.Postgresql ( ConnectionPool ) import Control.Monad.Reader.Has ( Has , ask ) @@ -182,8 +162,12 @@ getReleaseNotesR = do Nothing -> sendResponseStatus status400 (InvalidParamsE "get:id" "") Just package -> do appConnPool <- appConnPool <$> getYesod - (_, notes) <- runDB $ fetchAllAppVersions appConnPool (PkgId package) - pure notes + versionRecords <- runDB $ fetchAllAppVersions appConnPool (PkgId package) + pure $ constructReleaseNotesApiRes versionRecords + where + constructReleaseNotesApiRes :: [VersionRecord] -> ReleaseNotes + constructReleaseNotesApiRes vers = do + ReleaseNotes $ HM.fromList $ sortOn (Down) $ (versionRecordNumber &&& versionRecordReleaseNotes) <$> vers getEosR :: Handler TypedContent getEosR = do @@ -213,6 +197,7 @@ getEosR = do void $ insertUnique (EosHash v t) -- lazily populate pure t +-- TODO refactor with conduit getVersionLatestR :: Handler VersionLatestRes getVersionLatestR = do getParameters <- reqGetParams <$> getRequest @@ -240,13 +225,6 @@ getPackageListR = do Nothing -> const True Just v -> flip satisfies v pkgIds <- getPkgIdsQuery - -- deep info - -- generate data from db - -- filter os - -- filter from request - -- shallow info - generate get deps - -- transformations - -- assemble api response filteredPackages <- case pkgIds of Nothing -> do -- query for all @@ -258,8 +236,11 @@ getPackageListR = do $ runConduit $ searchServices category query .| zipVersions - .| mapC (\(a, vs) -> (,,) a vs Any) - .| filterOsCompatible osPredicate + .| zipCategories + -- if no packages are specified, the VersionRange is implicitly `*` + .| mapC (\(a, vs, cats) -> (a, vs, cats,Any)) + .| filterLatestVersionFromSpec + .| filterPkgOsCompatible osPredicate -- pages start at 1 for some reason. TODO: make pages start at 0 .| (dropC (limit' * (page - 1)) *> takeC limit') .| sinkList @@ -267,26 +248,21 @@ getPackageListR = do -- for each item in list get best available from version range let vMap = (packageReqId &&& packageReqVersion) <$> packages' runDB + -- TODO could probably be better with sequenceConduits . runConduit $ getPkgData (packageReqId <$> packages') .| zipVersions - .| mapC - (\(a, vs) -> - let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) vMap - in (a, filter ((<|| spec) . versionRecordNumber . entityVal) vs, spec) - ) - .| filterOsCompatible osPredicate + .| zipCategories + .| mapC (\(a, vs, cats) -> do + let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) vMap + (a, vs, cats, spec) + ) + .| filterLatestVersionFromSpec + .| filterPkgOsCompatible osPredicate .| sinkList - (keys, packageMetadata) <- runDB $ createPackageMetadata filteredPackages - appConnPool <- appConnPool <$> getYesod - serviceDetailResult <- mapConcurrently (getServiceDetails osPredicate appConnPool packageMetadata) keys - let (errors, res) = partitionEithers serviceDetailResult - case errors of - x : xs -> do - -- log all errors but just throw first error until Validation implemented - TODO https://hackage.haskell.org/package/validation - for_ xs (\e -> $logWarn [i|Get package list errors: #{e}|]) - sendResponseStatus (toStatus x) x - [] -> pure $ PackageListRes res + -- NOTE: if a package's dependencies do not meet the system requirements, it is currently omitted from the list + pkgsWithDependencies <- runDB $ mapConcurrently (getPackageDependencies osPredicate) filteredPackages + PackageListRes <$> mapConcurrently constructPackageListApiRes pkgsWithDependencies where defaults = PackageListDefaults { packageListOrder = DESC @@ -342,104 +318,36 @@ getPackageListR = do $logWarn (show e) sendResponseStatus status400 e Right v -> pure $ Just v - -mergeDupes :: ([Version], VersionRange) -> ([Version], VersionRange) -> ([Version], VersionRange) -mergeDupes (vs, vr) (vs', _) = (,) ((++) vs vs') vr - -createPackageMetadata :: (MonadReader r m, MonadIO m) - => [(Entity PkgRecord, [Entity VersionRecord], VersionRange)] - -> ReaderT - SqlBackend - m - ([PkgId], HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) -createPackageMetadata pkgs = do - let keys = unPkgRecordKey . entityKey . fst3 <$> pkgs - cats <- fetchAppCategories keys - let vers = - pkgs - <&> first3 (unPkgRecordKey . entityKey) - <&> second3 (sortOn Down . fmap (versionRecordNumber . entityVal)) - <&> (\(a, vs, vr) -> (,) a $ (,) vs vr) - & HM.fromListWith mergeDupes - pure $ (keys, HM.intersectionWith (,) vers (categoryName <<$>> cats)) - -getServiceDetails :: (MonadResource m, MonadReader r m, MonadLogger m, Has AppSettings r, MonadUnliftIO m) - => (Version -> Bool) - -> ConnectionPool - -> (HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle])) - -> PkgId - -> m (Either S9Error PackageRes) -getServiceDetails osPredicate appConnPool metadata pkg = runExceptT $ do - settings <- ask - packageMetadata <- case HM.lookup pkg metadata of - Nothing -> liftEither . Left $ NotFoundE [i|#{pkg} not found.|] - Just m -> pure m - let domain = registryHostname settings - let versionInfo = fst $ (HM.!) metadata pkg - version <- case snd versionInfo of - Any -> do - -- grab first value, which will be the latest version - case fst versionInfo of - [] -> liftEither . Left $ NotFoundE $ [i|No latest version found for #{pkg}|] - x : _ -> pure x - spec -> case headMay . sortOn Down $ filter (`satisfies` spec) $ fst versionInfo of - Nothing -> liftEither . Left $ NotFoundE [i|No version for #{pkg} satisfying #{spec}|] - Just v -> pure v - manifest <- flip runReaderT settings $ (snd <$> getManifest pkg version) >>= \bs -> - runConduit $ bs .| CL.foldMap BS.fromStrict - case eitherDecode manifest of - Left _ -> liftEither . Left $ AssetParseE [i|#{pkg}:manifest|] (decodeUtf8 $ BS.toStrict manifest) - Right m -> do - let depVerList = (fst &&& (packageDependencyVersion . snd)) <$> (HM.toList $ packageManifestDependencies m) - (_, depMetadata) <- lift $ runSqlPool (createPackageMetadata =<< getDependencies depVerList) appConnPool - let (errors, deps) = partitionEithers $ parMap - rpar - (mapDependencyMetadata domain $ (HM.union depMetadata metadata)) - (HM.toList $ packageManifestDependencies m) - case errors of - _ : xs -> liftEither . Left $ DepMetadataE xs - [] -> pure $ PackageRes { packageResIcon = [i|https://#{domain}/package/icon/#{pkg}|] - -- pass through raw JSON Value, we have checked its correct parsing above - , packageResManifest = unsafeFromJust . decode $ manifest - , packageResCategories = snd packageMetadata - , packageResInstructions = [i|https://#{domain}/package/instructions/#{pkg}|] - , packageResLicense = [i|https://#{domain}/package/license/#{pkg}|] - , packageResVersions = fst . fst $ packageMetadata - , packageResDependencies = HM.fromList deps - } - where - getDependencies :: (MonadResource m, MonadUnliftIO m) - => [(PkgId, VersionRange)] - -> ReaderT SqlBackend m [(Entity PkgRecord, [Entity VersionRecord], VersionRange)] - getDependencies deps = - runConduit - $ getPkgData (fst <$> deps) - .| zipVersions - .| mapC - (\(a, vs) -> - let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) deps - in (a, filter ((<|| spec) . versionRecordNumber . entityVal) vs, spec) - ) - .| filterOsCompatible osPredicate - .| sinkList - -mapDependencyMetadata :: Text - -> HM.HashMap PkgId (([Version], VersionRange), [CategoryTitle]) - -> (PkgId, PackageDependency) - -> Either Text (PkgId, DependencyRes) -mapDependencyMetadata domain metadata (appId, depInfo) = do - depMetadata <- case HM.lookup appId metadata of - Nothing -> Left [i|dependency metadata for #{appId} not found.|] - Just m -> pure m - -- get best version from VersionRange of dependency - let satisfactory = filter (<|| packageDependencyVersion depInfo) (fst . fst $ depMetadata) - let best = getMax <$> foldMap (Just . Max) satisfactory - version <- case best of - Nothing -> Left [i|No satisfactory version for dependent package #{appId}|] - Just v -> pure v - pure - ( appId - , DependencyRes { dependencyResTitle = appId - , dependencyResIcon = [i|https://#{domain}/package/icon/#{appId}?spec==#{version}|] + getPackageDependencies :: (MonadIO m, MonadLogger m) => (Version -> Bool) -> (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) -> ReaderT SqlBackend m (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) + getPackageDependencies osPredicate (pkg, pkgVersions, pkgCategories, pkgVersion) = do + let pkgId = entityKey pkg + let pkgVersions' = versionRecordNumber . entityVal <$> pkgVersions + let pkgCategories' = entityVal <$> pkgCategories + pkgDepInfo <- getPkgDependencyData pkgId pkgVersion + pkgDepInfoWithVersions <- traverse zipDependencyVersions pkgDepInfo + let compatiblePkgDepInfo = fmap (filterDependencyOsCompatible osPredicate) pkgDepInfoWithVersions + res <- catMaybes <$> traverse filterDependencyBestVersion compatiblePkgDepInfo + pure $ (pkgId, pkgCategories', pkgVersions', pkgVersion, res) + constructPackageListApiRes :: (Monad m, MonadResource m, MonadReader r m, Has AppSettings r) => (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) -> m PackageRes + constructPackageListApiRes (pkgKey, pkgCategories, pkgVersions, pkgVersion, dependencies) = do + settings <- ask + let pkgId = unPkgRecordKey pkgKey + let domain = registryHostname settings + manifest <- flip runReaderT settings $ (snd <$> getManifest pkgId pkgVersion) >>= \bs -> + runConduit $ bs .| CL.foldMap BS.fromStrict + pure $ PackageRes { packageResIcon = [i|https://#{domain}/package/icon/#{pkgId}|] + -- pass through raw JSON Value, we have checked its correct parsing above + , packageResManifest = unsafeFromJust . decode $ manifest + , packageResCategories = categoryName <$> pkgCategories + , packageResInstructions = [i|https://#{domain}/package/instructions/#{pkgId}|] + , packageResLicense = [i|https://#{domain}/package/license/#{pkgId}|] + , packageResVersions = pkgVersions + , packageResDependencies = HM.fromList $ constructDependenciesApiRes domain dependencies } - ) + constructDependenciesApiRes :: Text + -> [(Key PkgRecord, Text, Version)] + -> [(PkgId, DependencyRes)] + constructDependenciesApiRes domain deps = fmap (\(depKey, depTitle, depVersion) -> do + let depId = unPkgRecordKey depKey + (depId, DependencyRes { dependencyResTitle = depTitle, dependencyResIcon = [i|https://#{domain}/package/icon/#{depId}?spec==#{depVersion}|]})) deps + diff --git a/src/Handler/Types/Marketplace.hs b/src/Handler/Types/Marketplace.hs index 8aad326..3a9c148 100644 --- a/src/Handler/Types/Marketplace.hs +++ b/src/Handler/Types/Marketplace.hs @@ -1,15 +1,16 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DeriveGeneric #-} + module Handler.Types.Marketplace where -import Lib.Types.Category ( CategoryTitle ) import Data.Aeson +import qualified Data.HashMap.Internal.Strict as HM +import Lib.Types.AppIndex ( PkgId ) +import Lib.Types.Category ( CategoryTitle ) +import Lib.Types.Emver ( Version + , VersionRange + ) import Startlude import Yesod -import qualified Data.HashMap.Internal.Strict as HM -import Lib.Types.Emver ( VersionRange - , Version - ) -import Lib.Types.AppIndex ( PkgId ) type URL = Text @@ -22,12 +23,12 @@ instance ToContent CategoryRes where instance ToTypedContent CategoryRes where toTypedContent = toTypedContent . toJSON data PackageRes = PackageRes - { packageResIcon :: URL - , packageResManifest :: Data.Aeson.Value -- PackageManifest - , packageResCategories :: [CategoryTitle] - , packageResInstructions :: URL - , packageResLicense :: URL - , packageResVersions :: [Version] + { packageResIcon :: URL + , packageResManifest :: Data.Aeson.Value -- PackageManifest + , packageResCategories :: [CategoryTitle] + , packageResInstructions :: URL + , packageResLicense :: URL + , packageResVersions :: [Version] , packageResDependencies :: HM.HashMap PkgId DependencyRes } deriving (Show, Generic) @@ -60,7 +61,7 @@ instance FromJSON PackageRes where packageResDependencies <- o .: "dependency-metadata" pure PackageRes { .. } data DependencyRes = DependencyRes - { dependencyResTitle :: PkgId + { dependencyResTitle :: Text -- TODO switch to `Text` to display actual title in Marketplace. Confirm with FE that this will not break loading. Perhaps return title and id? , dependencyResIcon :: URL } deriving (Eq, Show) diff --git a/src/Lib/Error.hs b/src/Lib/Error.hs index acc85bf..5c9e9a6 100644 --- a/src/Lib/Error.hs +++ b/src/Lib/Error.hs @@ -8,7 +8,6 @@ import Startlude import Data.String.Interpolate.IsString import Network.HTTP.Types import Yesod.Core -import qualified Data.Text as T type S9ErrT m = ExceptT S9Error m @@ -18,7 +17,6 @@ data S9Error = | NotFoundE Text | InvalidParamsE Text Text | AssetParseE Text Text - | DepMetadataE [Text] deriving (Show, Eq) instance Exception S9Error @@ -31,9 +29,6 @@ toError = \case NotFoundE e -> Error NOT_FOUND [i|#{e}|] InvalidParamsE e m -> Error INVALID_PARAMS [i|Could not parse request parameters #{e}: #{m}|] AssetParseE asset found -> Error PARSE_ERROR [i|Could not parse #{asset}: #{found}|] - DepMetadataE errs -> do - let errorText = T.concat errs - Error NOT_FOUND [i|#{errorText}|] data ErrorCode = DATABASE_ERROR @@ -69,4 +64,3 @@ toStatus = \case NotFoundE _ -> status404 InvalidParamsE _ _ -> status400 AssetParseE _ _ -> status500 - DepMetadataE _ -> status404 diff --git a/src/Lib/PkgRepository.hs b/src/Lib/PkgRepository.hs index 185a127..672fdc8 100644 --- a/src/Lib/PkgRepository.hs +++ b/src/Lib/PkgRepository.hs @@ -31,19 +31,33 @@ import qualified Data.Attoparsec.Text as Atto import Data.ByteString ( readFile , writeFile ) +import qualified Data.HashMap.Strict as HM import Data.String.Interpolate.IsString ( i ) import qualified Data.Text as T +import Data.Time ( getCurrentTime ) +import Database.Esqueleto.Experimental + ( ConnectionPool + , insertUnique + , runSqlPool + ) import Lib.Error ( S9Error(NotFoundE) ) import qualified Lib.External.AppMgr as AppMgr -import Lib.Types.AppIndex ( PkgId(..) - , PackageManifest(packageManifestIcon) +import Lib.Types.AppIndex ( PackageManifest + ( packageManifestIcon + , packageManifestId + , packageManifestVersion + ) + , PkgId(..) + , packageDependencyVersion + , packageManifestDependencies ) import Lib.Types.Emver ( Version , VersionRange , parseVersion , satisfies ) +import Model import Startlude ( ($) , (&&) , (.) @@ -62,14 +76,18 @@ import Startlude ( ($) , MonadReader , Show , SomeException(..) + , Traversable(traverse) , filter , find + , first , for_ + , fst , headMay , not , partitionEithers , pure , show + , snd , sortOn , throwIO , void @@ -111,7 +129,6 @@ import Yesod.Core.Content ( typeGif , typeSvg ) import Yesod.Core.Types ( ContentType ) - data ManifestParseException = ManifestParseException FilePath deriving Show instance Exception ManifestParseException @@ -143,10 +160,28 @@ getBestVersion :: (MonadIO m, MonadReader r m, Has PkgRepo r, MonadLogger m) -> m (Maybe Version) getBestVersion pkg spec = headMay . sortOn Down <$> getViableVersions pkg spec --- TODO add loadDependencies +loadPkgDependencies :: MonadUnliftIO m => ConnectionPool -> PackageManifest -> m () +loadPkgDependencies appConnPool manifest = do + let pkgId = packageManifestId manifest + let pkgVersion = packageManifestVersion manifest + let deps = packageManifestDependencies manifest + time <- liftIO getCurrentTime + let deps' = first PkgRecordKey <$> HM.toList deps + _ <- traverse + (\d -> + (runSqlPool + ( insertUnique + $ PkgDependency time (PkgRecordKey pkgId) pkgVersion (fst d) (packageDependencyVersion . snd $ d) + ) + appConnPool + ) + ) + deps' + pure () + -- extract all package assets into their own respective files -extractPkg :: (MonadUnliftIO m, MonadReader r m, Has PkgRepo r, MonadLoggerIO m) => FilePath -> m () -extractPkg fp = handle @_ @SomeException cleanup $ do +extractPkg :: (MonadUnliftIO m, MonadReader r m, Has PkgRepo r, MonadLoggerIO m) => ConnectionPool -> FilePath -> m () +extractPkg pool fp = handle @_ @SomeException cleanup $ do $logInfo [i|Extracting package: #{fp}|] PkgRepo { pkgRepoAppMgrBin = appmgr } <- ask let pkgRoot = takeDirectory fp @@ -169,6 +204,7 @@ extractPkg fp = handle @_ @SomeException cleanup $ do Just x -> case takeExtension (T.unpack x) of "" -> "png" other -> other + loadPkgDependencies pool manifest liftIO $ renameFile (pkgRoot "icon.tmp") (pkgRoot iconDest) hash <- wait pkgHashTask liftIO $ writeFile (pkgRoot "hash.bin") hash @@ -184,8 +220,8 @@ extractPkg fp = handle @_ @SomeException cleanup $ do mapConcurrently_ (removeFile . (pkgRoot )) toRemove throwIO e -watchPkgRepoRoot :: (MonadUnliftIO m, MonadReader r m, Has PkgRepo r, MonadLoggerIO m) => m (IO Bool) -watchPkgRepoRoot = do +watchPkgRepoRoot :: (MonadUnliftIO m, MonadReader r m, Has PkgRepo r, MonadLoggerIO m) => ConnectionPool -> m (IO Bool) +watchPkgRepoRoot pool = do $logInfo "Starting FSNotify Watch Manager" root <- asks pkgRepoFileRoot runInIO <- askRunInIO @@ -195,7 +231,7 @@ watchPkgRepoRoot = do let pkg = eventPath evt -- TODO: validate that package path is an actual s9pk and is in a correctly conforming path. void . forkIO $ runInIO $ do - (extractPkg pkg) + (extractPkg pool pkg) takeMVar box stop pure $ tryPutMVar box () diff --git a/src/Lib/Types/AppIndex.hs b/src/Lib/Types/AppIndex.hs index a12f296..f45d97a 100644 --- a/src/Lib/Types/AppIndex.hs +++ b/src/Lib/Types/AppIndex.hs @@ -17,7 +17,6 @@ import Data.Aeson ( (.:) , ToJSON(..) , ToJSONKey(..) , withObject - , eitherDecode ) import qualified Data.ByteString.Lazy as BS import Data.Functor.Contravariant ( contramap ) @@ -77,7 +76,6 @@ data VersionInfo = VersionInfo } deriving (Eq, Show) --- TODO rename to PackageDependencyInfo data PackageDependency = PackageDependency { packageDependencyOptional :: Maybe Text , packageDependencyVersion :: VersionRange diff --git a/src/Model.hs b/src/Model.hs index 745def9..48472c4 100644 --- a/src/Model.hs +++ b/src/Model.hs @@ -17,7 +17,6 @@ import Lib.Types.Category import Lib.Types.Emver import Orphans.Emver ( ) import Startlude -import Yesod.Persist ( PersistFilter(Eq) ) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| PkgRecord @@ -92,13 +91,14 @@ ErrorLogRecord message Text incidents Word32 UniqueLogRecord epoch commitHash sourceFile line target level message + PkgDependency createdAt UTCTime pkgId PkgRecordId pkgVersion Version depId PkgRecordId depVersionRange VersionRange - UniquePkgVersion pkgId pkgVersion + UniquePkgDepVersion pkgId pkgVersion depId deriving Eq deriving Show |] diff --git a/src/Util/Shared.hs b/src/Util/Shared.hs index 0892f15..2a0ae29 100644 --- a/src/Util/Shared.hs +++ b/src/Util/Shared.hs @@ -1,20 +1,45 @@ {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE QuasiQuotes #-} module Util.Shared where -import Startlude hiding ( Handler ) +import Startlude hiding ( Handler + , yield + ) import qualified Data.Text as T import Network.HTTP.Types import Yesod.Core +import Conduit ( ConduitT + , awaitForever + , yield + ) import Control.Monad.Reader.Has ( Has ) +import Data.Semigroup ( Max(Max) + , getMax + ) +import Data.String.Interpolate.IsString + ( i ) +import Database.Esqueleto.Experimental + ( Entity + , Key + , entityKey + , entityVal + ) import Foundation import Lib.PkgRepository ( PkgRepo , getHash ) import Lib.Types.AppIndex ( PkgId ) import Lib.Types.Emver +import Model ( Category + , PkgDependency(pkgDependencyDepId, pkgDependencyDepVersionRange) + , PkgRecord(pkgRecordTitle) + , VersionRecord(versionRecordNumber, versionRecordOsVersion) + , pkgDependencyPkgId + ) getVersionSpecFromQuery :: Handler VersionRange getVersionSpecFromQuery = do @@ -32,3 +57,53 @@ orThrow :: MonadHandler m => m (Maybe a) -> m a -> m a orThrow action other = action >>= \case Nothing -> other Just x -> pure x + + +filterPkgOsCompatible :: Monad m + => (Version -> Bool) + -> ConduitT + (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) + (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) + m + () +filterPkgOsCompatible p = awaitForever $ \(app, versions, cats, requestedVersion) -> do + let compatible = filter (p . versionRecordOsVersion . entityVal) versions + when (not $ null compatible) $ yield (app, compatible, cats, requestedVersion) + +filterDependencyOsCompatible :: (Version -> Bool) + -> (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) + -> (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) +filterDependencyOsCompatible p (pkgDeps, pkg, versions) = do + let compatible = filter (p . versionRecordOsVersion . entityVal) versions + (pkgDeps, pkg, compatible) + +filterLatestVersionFromSpec :: (Monad m, MonadLogger m) + => ConduitT + (Entity PkgRecord, [Entity VersionRecord], [Entity Category], VersionRange) + (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) + m + () +filterLatestVersionFromSpec = awaitForever $ \(a, vs, cats, spec) -> do + let pkgId = entityKey a + case headMay . sortOn Down $ filter (`satisfies` spec) $ fmap (versionRecordNumber . entityVal) vs of + Nothing -> $logInfo [i|No version for #{pkgId} satisfying #{spec}|] + Just v -> yield $ (,,,) a vs cats v + +-- get best version of the dependency based on what is specified in the db (ie. what is specified in the manifest for the package) +filterDependencyBestVersion :: MonadLogger m + => (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) + -> m (Maybe (Key PkgRecord, Text, Version)) +filterDependencyBestVersion (pkgDepRecord, depPkgRecord, depVersions) = do + -- get best version from VersionRange of dependency + let pkgId = pkgDependencyPkgId $ entityVal pkgDepRecord + let depId = pkgDependencyDepId $ entityVal pkgDepRecord + let depTitle = pkgRecordTitle $ entityVal depPkgRecord + let satisfactory = filter (<|| (pkgDependencyDepVersionRange $ entityVal pkgDepRecord)) + (versionRecordNumber . entityVal <$> depVersions) + case getMax <$> foldMap (Just . Max) satisfactory of + -- QUESTION is this an acceptable transformation here? These are the only values that we care about after this filter. + Just bestVersion -> pure $ Just (depId, depTitle, bestVersion) + Nothing -> do + $logInfo [i|No satisfactory version of #{depId} for dependent package #{pkgId}|] + -- TODO it would be better if we could return the requirements for display + pure Nothing diff --git a/stack.yaml b/stack.yaml index 9739af5..14d4398 100644 --- a/stack.yaml +++ b/stack.yaml @@ -44,6 +44,7 @@ extra-deps: - esqueleto-3.5.1.0 - monad-logger-extras-0.1.1.1 - wai-request-spec-0.10.2.4 + - data-tree-print-0.1.0.2 # Override default flag values for local packages and extra-deps # flags: {} diff --git a/test/Handler/AppSpec.hs b/test/Handler/AppSpec.hs index b798a09..8951849 100644 --- a/test/Handler/AppSpec.hs +++ b/test/Handler/AppSpec.hs @@ -3,21 +3,19 @@ module Handler.AppSpec ( spec - ) -where + ) where -import Startlude -import Database.Persist.Sql import Data.Maybe +import Database.Persist.Sql +import Startlude -import TestImport -import Model -import Handler.Marketplace -import Seed -import Lib.Types.AppIndex import Data.Aeson import Data.Either.Extra -import Handler.Marketplace ( PackageRes ) +import Handler.Types.Marketplace ( PackageRes(packageResDependencies, packageResManifest) ) +import Lib.Types.AppIndex +import Model +import Seed +import TestImport spec :: Spec spec = do @@ -92,13 +90,13 @@ spec = do setMethod "GET" setUrl ("/package/bitcoind.s9pk?spec==0.20.0" :: Text) statusIs 404 - xdescribe "GET /package/:pkgId with unknown package" $ withApp $ it "fails to get an unregistered app" $ do + describe "GET /package/:pkgId with unknown package" $ withApp $ it "fails to get an unregistered app" $ do _ <- seedBitcoinLndStack request $ do setMethod "GET" setUrl ("/package/tempapp.s9pk?spec=0.0.1" :: Text) statusIs 404 - xdescribe "GET /package/:pkgId with package at unknown version" + describe "GET /package/:pkgId with package at unknown version" $ withApp $ it "fails to get an unregistered app" $ do diff --git a/test/Seed.hs b/test/Seed.hs index ee831f9..90c202e 100644 --- a/test/Seed.hs +++ b/test/Seed.hs @@ -1,25 +1,27 @@ module Seed where -import Startlude ( ($) - , Applicative(pure) - , Maybe(Nothing, Just) - , getCurrentTime - , MonadIO(liftIO) - ) -import Database.Persist.Sql ( PersistStoreWrite(insert_, insertKey, insert) ) -import Model ( Key(PkgRecordKey) - , PkgRecord(PkgRecord) - , Category(Category) +import Database.Persist.Sql ( PersistStoreWrite(insert, insertKey, insert_) ) +import Model ( Category(Category) + , Key(PkgRecordKey) , PkgCategory(PkgCategory) + , PkgDependency(PkgDependency) + , PkgRecord(PkgRecord) , VersionRecord(VersionRecord) ) +import Startlude ( ($) + , Applicative(pure) + , Maybe(Just, Nothing) + , MonadIO(liftIO) + , getCurrentTime + ) -import TestImport ( runDBtest - , RegistryCtx +import Lib.Types.Category ( CategoryTitle(BITCOIN, FEATURED, LIGHTNING) ) +import Prelude ( read ) +import TestImport ( RegistryCtx , SIO , YesodExampleData + , runDBtest ) -import Lib.Types.Category ( CategoryTitle(LIGHTNING, FEATURED, BITCOIN) ) seedBitcoinLndStack :: SIO (YesodExampleData RegistryCtx) () seedBitcoinLndStack = do @@ -73,4 +75,14 @@ seedBitcoinLndStack = do _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "lnd") btcCat _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "bitcoind") btcCat _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "btc-rpc-proxy") btcCat + _ <- runDBtest $ insert_ $ PkgDependency time + (PkgRecordKey "lnd") + "0.13.3.1" + (PkgRecordKey "bitcoind") + (read ">=0.21.1.2 <0.22.0") + _ <- runDBtest $ insert_ $ PkgDependency time + (PkgRecordKey "lnd") + "0.13.3.1" + (PkgRecordKey "btc-rpc-proxy") + (read ">=0.3.2.1 <0.4.0") pure () From e7708da1227c9331f0e49586e5ba83f1c3d363fc Mon Sep 17 00:00:00 2001 From: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Date: Thu, 2 Dec 2021 16:41:02 -0700 Subject: [PATCH 11/12] resolve PR feedback - add record type for package and dependency metadata --- README.md | 2 +- src/Database/Marketplace.hs | 14 ++++- src/Handler/Marketplace.hs | 24 +++----- src/Handler/Types/Marketplace.hs | 18 ++++++ src/Lib/PkgRepository.hs | 8 +-- src/Util/Shared.hs | 102 +++++++++++++++++++------------ stack.yaml | 1 - test/Handler/AppSpec.hs | 29 ++++++--- test/Seed.hs | 87 ++++++++------------------ 9 files changed, 153 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index cd63d16..b7c9939 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ stack test --flag start9-registry:library-only --flag start9-registry:dev ## Builds -`stack build --copy-bins --local-bin-path=dist` +`make` ### Tests with HIE Setup - install hspec-discover globally `cabal install hspec-discover` (requires cabal installation) diff --git a/src/Database/Marketplace.hs b/src/Database/Marketplace.hs index a87ea80..6ca1ce1 100644 --- a/src/Database/Marketplace.hs +++ b/src/Database/Marketplace.hs @@ -39,6 +39,13 @@ import Database.Persist.Postgresql , selectSource , (||.) ) +import Handler.Types.Marketplace ( PackageDependencyMetadata + ( PackageDependencyMetadata + , packageDependencyMetadataDepPkgRecord + , packageDependencyMetadataDepVersions + , packageDependencyMetadataPkgDependencyRecord + ) + ) import Lib.Types.AppIndex ( PkgId ) import Lib.Types.Category import Lib.Types.Emver ( Version ) @@ -143,14 +150,17 @@ zipVersions = awaitForever $ \pkg -> do zipDependencyVersions :: (Monad m, MonadIO m) => (Entity PkgDependency, Entity PkgRecord) - -> ReaderT SqlBackend m (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) + -> ReaderT SqlBackend m PackageDependencyMetadata zipDependencyVersions (pkgDepRecord, depRecord) = do let pkgDbId = entityKey $ depRecord depVers <- select $ do v <- from $ table @VersionRecord where_ $ v ^. VersionRecordPkgId ==. val pkgDbId pure v - pure $ (pkgDepRecord, depRecord, depVers) + pure $ PackageDependencyMetadata { packageDependencyMetadataPkgDependencyRecord = pkgDepRecord + , packageDependencyMetadataDepPkgRecord = depRecord + , packageDependencyMetadataDepVersions = depVers + } fetchAllAppVersions :: MonadUnliftIO m => ConnectionPool -> PkgId -> m [VersionRecord] fetchAllAppVersions appConnPool appId = do diff --git a/src/Handler/Marketplace.hs b/src/Handler/Marketplace.hs index 96da839..3a910ab 100644 --- a/src/Handler/Marketplace.hs +++ b/src/Handler/Marketplace.hs @@ -20,7 +20,6 @@ import Startlude hiding ( Any import Conduit ( (.|) , awaitForever , dropC - , mapC , runConduit , sinkList , sourceFile @@ -81,10 +80,9 @@ import Lib.Types.AppIndex ( PkgId(PkgId) import Lib.Types.AppIndex ( ) import Lib.Types.Category ( CategoryTitle(..) ) import Lib.Types.Emver ( Version - , VersionRange(Any) , parseRange , parseVersion - , satisfies + , satisfies, VersionRange ) import Model ( Category(..) , EntityField(..) @@ -167,7 +165,7 @@ getReleaseNotesR = do where constructReleaseNotesApiRes :: [VersionRecord] -> ReleaseNotes constructReleaseNotesApiRes vers = do - ReleaseNotes $ HM.fromList $ sortOn (Down) $ (versionRecordNumber &&& versionRecordReleaseNotes) <$> vers + ReleaseNotes $ HM.fromList $ sortOn Down $ (versionRecordNumber &&& versionRecordReleaseNotes) <$> vers getEosR :: Handler TypedContent getEosR = do @@ -237,9 +235,8 @@ getPackageListR = do $ searchServices category query .| zipVersions .| zipCategories - -- if no packages are specified, the VersionRange is implicitly `*` - .| mapC (\(a, vs, cats) -> (a, vs, cats,Any)) - .| filterLatestVersionFromSpec + -- empty list since there are no requested packages in this case + .| filterLatestVersionFromSpec [] .| filterPkgOsCompatible osPredicate -- pages start at 1 for some reason. TODO: make pages start at 0 .| (dropC (limit' * (page - 1)) *> takeC limit') @@ -253,11 +250,7 @@ getPackageListR = do $ getPkgData (packageReqId <$> packages') .| zipVersions .| zipCategories - .| mapC (\(a, vs, cats) -> do - let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) vMap - (a, vs, cats, spec) - ) - .| filterLatestVersionFromSpec + .| filterLatestVersionFromSpec vMap .| filterPkgOsCompatible osPredicate .| sinkList -- NOTE: if a package's dependencies do not meet the system requirements, it is currently omitted from the list @@ -318,8 +311,8 @@ getPackageListR = do $logWarn (show e) sendResponseStatus status400 e Right v -> pure $ Just v - getPackageDependencies :: (MonadIO m, MonadLogger m) => (Version -> Bool) -> (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) -> ReaderT SqlBackend m (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) - getPackageDependencies osPredicate (pkg, pkgVersions, pkgCategories, pkgVersion) = do + getPackageDependencies :: (MonadIO m, MonadLogger m) => (Version -> Bool) -> PackageMetadata -> ReaderT SqlBackend m (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) + getPackageDependencies osPredicate PackageMetadata { packageMetadataPkgRecord = pkg, packageMetadataPkgVersionRecords = pkgVersions, packageMetadataPkgCategories = pkgCategories, packageMetadataPkgVersion = pkgVersion} = do let pkgId = entityKey pkg let pkgVersions' = versionRecordNumber . entityVal <$> pkgVersions let pkgCategories' = entityVal <$> pkgCategories @@ -328,7 +321,7 @@ getPackageListR = do let compatiblePkgDepInfo = fmap (filterDependencyOsCompatible osPredicate) pkgDepInfoWithVersions res <- catMaybes <$> traverse filterDependencyBestVersion compatiblePkgDepInfo pure $ (pkgId, pkgCategories', pkgVersions', pkgVersion, res) - constructPackageListApiRes :: (Monad m, MonadResource m, MonadReader r m, Has AppSettings r) => (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) -> m PackageRes + constructPackageListApiRes :: (MonadResource m, MonadReader r m, Has AppSettings r) => (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) -> m PackageRes constructPackageListApiRes (pkgKey, pkgCategories, pkgVersions, pkgVersion, dependencies) = do settings <- ask let pkgId = unPkgRecordKey pkgKey @@ -350,4 +343,3 @@ getPackageListR = do constructDependenciesApiRes domain deps = fmap (\(depKey, depTitle, depVersion) -> do let depId = unPkgRecordKey depKey (depId, DependencyRes { dependencyResTitle = depTitle, dependencyResIcon = [i|https://#{domain}/package/icon/#{depId}?spec==#{depVersion}|]})) deps - diff --git a/src/Handler/Types/Marketplace.hs b/src/Handler/Types/Marketplace.hs index 3a9c148..c57f753 100644 --- a/src/Handler/Types/Marketplace.hs +++ b/src/Handler/Types/Marketplace.hs @@ -9,6 +9,11 @@ import Lib.Types.Category ( CategoryTitle ) import Lib.Types.Emver ( Version , VersionRange ) +import Model ( Category + , PkgDependency + , PkgRecord + , VersionRecord + ) import Startlude import Yesod @@ -121,3 +126,16 @@ instance FromJSON PackageReq where packageReqId <- o .: "id" packageReqVersion <- o .: "version" pure PackageReq { .. } +data PackageMetadata = PackageMetadata + { packageMetadataPkgRecord :: Entity PkgRecord + , packageMetadataPkgVersionRecords :: [Entity VersionRecord] + , packageMetadataPkgCategories :: [Entity Category] + , packageMetadataPkgVersion :: Version + } + deriving (Eq, Show) +data PackageDependencyMetadata = PackageDependencyMetadata + { packageDependencyMetadataPkgDependencyRecord :: Entity PkgDependency + , packageDependencyMetadataDepPkgRecord :: Entity PkgRecord + , packageDependencyMetadataDepVersions :: [Entity VersionRecord] + } + deriving (Eq, Show) diff --git a/src/Lib/PkgRepository.hs b/src/Lib/PkgRepository.hs index 672fdc8..6a59812 100644 --- a/src/Lib/PkgRepository.hs +++ b/src/Lib/PkgRepository.hs @@ -76,7 +76,6 @@ import Startlude ( ($) , MonadReader , Show , SomeException(..) - , Traversable(traverse) , filter , find , first @@ -167,7 +166,8 @@ loadPkgDependencies appConnPool manifest = do let deps = packageManifestDependencies manifest time <- liftIO getCurrentTime let deps' = first PkgRecordKey <$> HM.toList deps - _ <- traverse + for_ + deps' (\d -> (runSqlPool ( insertUnique @@ -176,8 +176,6 @@ loadPkgDependencies appConnPool manifest = do appConnPool ) ) - deps' - pure () -- extract all package assets into their own respective files extractPkg :: (MonadUnliftIO m, MonadReader r m, Has PkgRepo r, MonadLoggerIO m) => ConnectionPool -> FilePath -> m () @@ -231,7 +229,7 @@ watchPkgRepoRoot pool = do let pkg = eventPath evt -- TODO: validate that package path is an actual s9pk and is in a correctly conforming path. void . forkIO $ runInIO $ do - (extractPkg pool pkg) + extractPkg pool pkg takeMVar box stop pure $ tryPutMVar box () diff --git a/src/Util/Shared.hs b/src/Util/Shared.hs index 2a0ae29..5faf3ba 100644 --- a/src/Util/Shared.hs +++ b/src/Util/Shared.hs @@ -4,7 +4,8 @@ module Util.Shared where -import Startlude hiding ( Handler +import Startlude hiding ( Any + , Handler , yield ) @@ -29,12 +30,28 @@ import Database.Esqueleto.Experimental , entityVal ) import Foundation +import GHC.List ( lookup ) +import Handler.Types.Marketplace ( PackageDependencyMetadata + ( PackageDependencyMetadata + , packageDependencyMetadataDepPkgRecord + , packageDependencyMetadataDepVersions + , packageDependencyMetadataPkgDependencyRecord + ) + , PackageMetadata + ( PackageMetadata + , packageMetadataPkgCategories + , packageMetadataPkgRecord + , packageMetadataPkgVersion + , packageMetadataPkgVersionRecords + ) + ) import Lib.PkgRepository ( PkgRepo , getHash ) import Lib.Types.AppIndex ( PkgId ) import Lib.Types.Emver import Model ( Category + , Key(unPkgRecordKey) , PkgDependency(pkgDependencyDepId, pkgDependencyDepVersionRange) , PkgRecord(pkgRecordTitle) , VersionRecord(versionRecordNumber, versionRecordOsVersion) @@ -59,51 +76,60 @@ orThrow action other = action >>= \case Just x -> pure x -filterPkgOsCompatible :: Monad m - => (Version -> Bool) - -> ConduitT - (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) - (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) - m - () -filterPkgOsCompatible p = awaitForever $ \(app, versions, cats, requestedVersion) -> do - let compatible = filter (p . versionRecordOsVersion . entityVal) versions - when (not $ null compatible) $ yield (app, compatible, cats, requestedVersion) +filterPkgOsCompatible :: Monad m => (Version -> Bool) -> ConduitT PackageMetadata PackageMetadata m () +filterPkgOsCompatible p = + awaitForever + $ \PackageMetadata { packageMetadataPkgRecord = pkg, packageMetadataPkgVersionRecords = versions, packageMetadataPkgCategories = cats, packageMetadataPkgVersion = requestedVersion } -> + do + let compatible = filter (p . versionRecordOsVersion . entityVal) versions + when (not $ null compatible) $ yield PackageMetadata { packageMetadataPkgRecord = pkg + , packageMetadataPkgVersionRecords = compatible + , packageMetadataPkgCategories = cats + , packageMetadataPkgVersion = requestedVersion + } -filterDependencyOsCompatible :: (Version -> Bool) - -> (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) - -> (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) -filterDependencyOsCompatible p (pkgDeps, pkg, versions) = do - let compatible = filter (p . versionRecordOsVersion . entityVal) versions - (pkgDeps, pkg, compatible) +filterDependencyOsCompatible :: (Version -> Bool) -> PackageDependencyMetadata -> PackageDependencyMetadata +filterDependencyOsCompatible p PackageDependencyMetadata { packageDependencyMetadataPkgDependencyRecord = pkgDeps, packageDependencyMetadataDepPkgRecord = pkg, packageDependencyMetadataDepVersions = depVersions } + = do + let compatible = filter (p . versionRecordOsVersion . entityVal) depVersions + PackageDependencyMetadata { packageDependencyMetadataPkgDependencyRecord = pkgDeps + , packageDependencyMetadataDepPkgRecord = pkg + , packageDependencyMetadataDepVersions = compatible + } filterLatestVersionFromSpec :: (Monad m, MonadLogger m) - => ConduitT - (Entity PkgRecord, [Entity VersionRecord], [Entity Category], VersionRange) - (Entity PkgRecord, [Entity VersionRecord], [Entity Category], Version) + => [(PkgId, VersionRange)] + -> ConduitT + (Entity PkgRecord, [Entity VersionRecord], [Entity Category]) + PackageMetadata m () -filterLatestVersionFromSpec = awaitForever $ \(a, vs, cats, spec) -> do +filterLatestVersionFromSpec versionMap = awaitForever $ \(a, vs, cats) -> do let pkgId = entityKey a + -- if no packages are specified, the VersionRange is implicitly `*` + let spec = fromMaybe Any $ lookup (unPkgRecordKey $ entityKey a) versionMap case headMay . sortOn Down $ filter (`satisfies` spec) $ fmap (versionRecordNumber . entityVal) vs of Nothing -> $logInfo [i|No version for #{pkgId} satisfying #{spec}|] - Just v -> yield $ (,,,) a vs cats v + Just v -> yield $ PackageMetadata { packageMetadataPkgRecord = a + , packageMetadataPkgVersionRecords = vs + , packageMetadataPkgCategories = cats + , packageMetadataPkgVersion = v + } -- get best version of the dependency based on what is specified in the db (ie. what is specified in the manifest for the package) -filterDependencyBestVersion :: MonadLogger m - => (Entity PkgDependency, Entity PkgRecord, [Entity VersionRecord]) - -> m (Maybe (Key PkgRecord, Text, Version)) -filterDependencyBestVersion (pkgDepRecord, depPkgRecord, depVersions) = do +filterDependencyBestVersion :: MonadLogger m => PackageDependencyMetadata -> m (Maybe (Key PkgRecord, Text, Version)) +filterDependencyBestVersion PackageDependencyMetadata { packageDependencyMetadataPkgDependencyRecord = pkgDepRecord, packageDependencyMetadataDepPkgRecord = depRecord, packageDependencyMetadataDepVersions = depVersions } + = do -- get best version from VersionRange of dependency - let pkgId = pkgDependencyPkgId $ entityVal pkgDepRecord - let depId = pkgDependencyDepId $ entityVal pkgDepRecord - let depTitle = pkgRecordTitle $ entityVal depPkgRecord - let satisfactory = filter (<|| (pkgDependencyDepVersionRange $ entityVal pkgDepRecord)) - (versionRecordNumber . entityVal <$> depVersions) - case getMax <$> foldMap (Just . Max) satisfactory of - -- QUESTION is this an acceptable transformation here? These are the only values that we care about after this filter. - Just bestVersion -> pure $ Just (depId, depTitle, bestVersion) - Nothing -> do - $logInfo [i|No satisfactory version of #{depId} for dependent package #{pkgId}|] - -- TODO it would be better if we could return the requirements for display - pure Nothing + let pkgId = pkgDependencyPkgId $ entityVal pkgDepRecord + let depId = pkgDependencyDepId $ entityVal pkgDepRecord + let depTitle = pkgRecordTitle $ entityVal depRecord + let satisfactory = filter (<|| (pkgDependencyDepVersionRange $ entityVal pkgDepRecord)) + (versionRecordNumber . entityVal <$> depVersions) + case getMax <$> foldMap (Just . Max) satisfactory of + -- QUESTION is this an acceptable transformation here? These are the only values that we care about after this filter. + Just bestVersion -> pure $ Just (depId, depTitle, bestVersion) + Nothing -> do + $logInfo [i|No satisfactory version of #{depId} for dependent package #{pkgId}|] + -- TODO it would be better if we could return the requirements for display + pure Nothing diff --git a/stack.yaml b/stack.yaml index 14d4398..9739af5 100644 --- a/stack.yaml +++ b/stack.yaml @@ -44,7 +44,6 @@ extra-deps: - esqueleto-3.5.1.0 - monad-logger-extras-0.1.1.1 - wai-request-spec-0.10.2.4 - - data-tree-print-0.1.0.2 # Override default flag values for local packages and extra-deps # flags: {} diff --git a/test/Handler/AppSpec.hs b/test/Handler/AppSpec.hs index 8951849..eafa7de 100644 --- a/test/Handler/AppSpec.hs +++ b/test/Handler/AppSpec.hs @@ -1,5 +1,6 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE QuasiQuotes #-} module Handler.AppSpec ( spec @@ -10,7 +11,9 @@ import Database.Persist.Sql import Startlude import Data.Aeson -import Data.Either.Extra +import Data.Aeson.Types ( parseEither ) +import Data.String.Interpolate.IsString + ( i ) import Handler.Types.Marketplace ( PackageRes(packageResDependencies, packageResManifest) ) import Lib.Types.AppIndex import Model @@ -35,8 +38,10 @@ spec = do statusIs 200 (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 - let pkg = fromJust $ head res - let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + let pkg = fromJust $ head res + (manifest :: PackageManifest) <- either (\e -> panic [i|failed to parse package manifest: #{e}|]) + pure + (parseEither parseJSON $ packageResManifest pkg) assertEq "manifest id should be bitcoind" (packageManifestId manifest) "bitcoind" describe "GET /package/index?ids" $ withApp @@ -59,8 +64,10 @@ spec = do statusIs 200 (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 - let pkg = fromJust $ head res - let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + let pkg = fromJust $ head res + (manifest :: PackageManifest) <- either (\e -> panic [i|failed to parse package manifest: #{e}|]) + pure + (parseEither parseJSON $ packageResManifest pkg) assertEq "manifest version should be 0.21.1.1" (packageManifestVersion manifest) "0.21.1.1" describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version or greater" $ do _ <- seedBitcoinLndStack @@ -70,8 +77,10 @@ spec = do statusIs 200 (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 - let pkg = fromJust $ head res - let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + let pkg = fromJust $ head res + (manifest :: PackageManifest) <- either (\e -> panic [i|failed to parse package manifest: #{e}|]) + pure + (parseEither parseJSON $ packageResManifest pkg) assertEq "manifest version should be 0.21.1.2" (packageManifestVersion manifest) "0.21.1.2" describe "GET /package/index?ids" $ withApp $ it "returns list of packages at specified version or greater" $ do _ <- seedBitcoinLndStack @@ -81,8 +90,10 @@ spec = do statusIs 200 (res :: [PackageRes]) <- requireJSONResponse assertEq "response should have one package" (length res) 1 - let pkg = fromJust $ head res - let (manifest :: PackageManifest) = fromRight' $ eitherDecode $ encode $ packageResManifest pkg + let pkg = fromJust $ head res + (manifest :: PackageManifest) <- either (\e -> panic [i|failed to parse package manifest: #{e}|]) + pure + (parseEither parseJSON $ packageResManifest pkg) assertEq "manifest version should be 0.21.1.2" (packageManifestVersion manifest) "0.21.1.2" describe "GET /package/:pkgId with unknown version spec for bitcoind" $ withApp $ it "fails to get unknown app" $ do _ <- seedBitcoinLndStack diff --git a/test/Seed.hs b/test/Seed.hs index 90c202e..e7ec0a5 100644 --- a/test/Seed.hs +++ b/test/Seed.hs @@ -24,65 +24,32 @@ import TestImport ( RegistryCtx ) seedBitcoinLndStack :: SIO (YesodExampleData RegistryCtx) () -seedBitcoinLndStack = do +seedBitcoinLndStack = runDBtest $ do time <- liftIO getCurrentTime - _ <- runDBtest $ insertKey (PkgRecordKey "bitcoind") $ PkgRecord time - (Just time) - "Bitcoin Core" - "short desc bitcoin" - "long desc bitcoin" - "png" - _ <- runDBtest $ insert $ VersionRecord time - (Just time) - (PkgRecordKey "bitcoind") - "0.21.1.2" - "notes" - "0.3.0" - Nothing - _ <- runDBtest $ insert $ VersionRecord time - (Just time) - (PkgRecordKey "bitcoind") - "0.21.1.1" - "notes" - "0.3.0" - Nothing - _ <- runDBtest $ insertKey (PkgRecordKey "lnd") $ PkgRecord time - (Just time) - "Lightning Network Daemon" - "short desc lnd" - "long desc lnd" - "png" - _ <- runDBtest $ insert $ VersionRecord time (Just time) (PkgRecordKey "lnd") "0.13.3.0" "notes" "0.3.0" Nothing - _ <- runDBtest $ insert $ VersionRecord time (Just time) (PkgRecordKey "lnd") "0.13.3.1" "notes" "0.3.0" Nothing - _ <- runDBtest $ insertKey (PkgRecordKey "btc-rpc-proxy") $ PkgRecord time - (Just time) - "BTC RPC Proxy" - "short desc btc-rpc-proxy" - "long desc btc-rpc-proxy" - "png" - _ <- runDBtest $ insert $ VersionRecord time - (Just time) - (PkgRecordKey "btc-rpc-proxy") - "0.3.2.1" - "notes" - "0.3.0" - Nothing - featuredCat <- runDBtest $ insert $ Category time FEATURED Nothing "desc" 0 - btcCat <- runDBtest $ insert $ Category time BITCOIN Nothing "desc" 0 - lnCat <- runDBtest $ insert $ Category time LIGHTNING Nothing "desc" 0 - _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "bitcoind") featuredCat - _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "lnd") lnCat - _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "lnd") btcCat - _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "bitcoind") btcCat - _ <- runDBtest $ insert_ $ PkgCategory time (PkgRecordKey "btc-rpc-proxy") btcCat - _ <- runDBtest $ insert_ $ PkgDependency time - (PkgRecordKey "lnd") - "0.13.3.1" - (PkgRecordKey "bitcoind") - (read ">=0.21.1.2 <0.22.0") - _ <- runDBtest $ insert_ $ PkgDependency time - (PkgRecordKey "lnd") - "0.13.3.1" - (PkgRecordKey "btc-rpc-proxy") - (read ">=0.3.2.1 <0.4.0") + insertKey (PkgRecordKey "bitcoind") + $ PkgRecord time (Just time) "Bitcoin Core" "short desc bitcoin" "long desc bitcoin" "png" + _ <- insert $ VersionRecord time (Just time) (PkgRecordKey "bitcoind") "0.21.1.2" "notes" "0.3.0" Nothing + _ <- insert $ VersionRecord time (Just time) (PkgRecordKey "bitcoind") "0.21.1.1" "notes" "0.3.0" Nothing + _ <- insertKey (PkgRecordKey "lnd") + $ PkgRecord time (Just time) "Lightning Network Daemon" "short desc lnd" "long desc lnd" "png" + _ <- insert $ VersionRecord time (Just time) (PkgRecordKey "lnd") "0.13.3.0" "notes" "0.3.0" Nothing + _ <- insert $ VersionRecord time (Just time) (PkgRecordKey "lnd") "0.13.3.1" "notes" "0.3.0" Nothing + _ <- insertKey (PkgRecordKey "btc-rpc-proxy") + $ PkgRecord time (Just time) "BTC RPC Proxy" "short desc btc-rpc-proxy" "long desc btc-rpc-proxy" "png" + _ <- insert $ VersionRecord time (Just time) (PkgRecordKey "btc-rpc-proxy") "0.3.2.1" "notes" "0.3.0" Nothing + featuredCat <- insert $ Category time FEATURED Nothing "desc" 0 + btcCat <- insert $ Category time BITCOIN Nothing "desc" 0 + lnCat <- insert $ Category time LIGHTNING Nothing "desc" 0 + _ <- insert_ $ PkgCategory time (PkgRecordKey "bitcoind") featuredCat + _ <- insert_ $ PkgCategory time (PkgRecordKey "lnd") lnCat + _ <- insert_ $ PkgCategory time (PkgRecordKey "lnd") btcCat + _ <- insert_ $ PkgCategory time (PkgRecordKey "bitcoind") btcCat + _ <- insert_ $ PkgCategory time (PkgRecordKey "btc-rpc-proxy") btcCat + _ <- insert_ + $ PkgDependency time (PkgRecordKey "lnd") "0.13.3.1" (PkgRecordKey "bitcoind") (read ">=0.21.1.2 <0.22.0") + _ <- insert_ $ PkgDependency time + (PkgRecordKey "lnd") + "0.13.3.1" + (PkgRecordKey "btc-rpc-proxy") + (read ">=0.3.2.1 <0.4.0") pure () From 7344a616a8e2bac2d6e63ab90abb389f92d3e05c Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 3 Dec 2021 10:29:45 -0700 Subject: [PATCH 12/12] Condense Imports --- src/Database/Marketplace.hs | 7 +------ src/Lib/PkgRepository.hs | 6 +----- src/Util/Shared.hs | 15 ++------------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/Database/Marketplace.hs b/src/Database/Marketplace.hs index 6ca1ce1..8eaa221 100644 --- a/src/Database/Marketplace.hs +++ b/src/Database/Marketplace.hs @@ -39,12 +39,7 @@ import Database.Persist.Postgresql , selectSource , (||.) ) -import Handler.Types.Marketplace ( PackageDependencyMetadata - ( PackageDependencyMetadata - , packageDependencyMetadataDepPkgRecord - , packageDependencyMetadataDepVersions - , packageDependencyMetadataPkgDependencyRecord - ) +import Handler.Types.Marketplace ( PackageDependencyMetadata(..) ) import Lib.Types.AppIndex ( PkgId ) import Lib.Types.Category diff --git a/src/Lib/PkgRepository.hs b/src/Lib/PkgRepository.hs index 6a59812..0463542 100644 --- a/src/Lib/PkgRepository.hs +++ b/src/Lib/PkgRepository.hs @@ -43,11 +43,7 @@ import Database.Esqueleto.Experimental ) import Lib.Error ( S9Error(NotFoundE) ) import qualified Lib.External.AppMgr as AppMgr -import Lib.Types.AppIndex ( PackageManifest - ( packageManifestIcon - , packageManifestId - , packageManifestVersion - ) +import Lib.Types.AppIndex ( PackageManifest(..) , PkgId(..) , packageDependencyVersion , packageManifestDependencies diff --git a/src/Util/Shared.hs b/src/Util/Shared.hs index 5faf3ba..88f30d9 100644 --- a/src/Util/Shared.hs +++ b/src/Util/Shared.hs @@ -31,19 +31,8 @@ import Database.Esqueleto.Experimental ) import Foundation import GHC.List ( lookup ) -import Handler.Types.Marketplace ( PackageDependencyMetadata - ( PackageDependencyMetadata - , packageDependencyMetadataDepPkgRecord - , packageDependencyMetadataDepVersions - , packageDependencyMetadataPkgDependencyRecord - ) - , PackageMetadata - ( PackageMetadata - , packageMetadataPkgCategories - , packageMetadataPkgRecord - , packageMetadataPkgVersion - , packageMetadataPkgVersionRecords - ) +import Handler.Types.Marketplace ( PackageDependencyMetadata(..) + , PackageMetadata(..) ) import Lib.PkgRepository ( PkgRepo , getHash