diff --git a/.gitignore b/.gitignore index 1069cbed5..28511d2f3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ firebase.json *.tfstate *.tfstate.backup +*.tfstate.lock* result result-* diff --git a/infra/default.nix b/infra/default.nix index d80686b90..be9ff89eb 100644 --- a/infra/default.nix +++ b/infra/default.nix @@ -1,12 +1,60 @@ with { pkgs = import ./nix {}; }; -# TODO: -# - plug DynamoDBLocal in tests -# -> https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html - rec { function = - pkgs.wai-lambda.wai-lambda-js-build-lambda "${handler}/bin/deckdeckgo-handler"; + pkgs.wai-lambda.wai-lambda-js-build-lambda "${handlerStatic}/bin/handler"; + + handlerStatic = pkgs.haskellPackagesStatic.deckdeckgo-handler; + handler = pkgs.haskellPackages.deckdeckgo-handler; + + dynamoJar = pkgs.runCommand "dynamodb-jar" { buildInputs = [ pkgs.gnutar ]; } + '' + mkdir -p $out + cd $out + tar -xvf ${pkgs.sources.dynamodb} + ''; + + test = pkgs.runCommand "tests" { buildInputs = [ pkgs.jre pkgs.curl pkgs.netcat pkgs.awscli ]; } + '' + + java -Djava.library.path=${dynamoJar}/DynamoDBLocal_lib -jar ${dynamoJar}/DynamoDBLocal.jar -sharedDb -port 8000 & + + while ! nc -z 127.0.0.1 8000; do + echo waiting for DynamoDB + sleep 1 + done + export AWS_DEFAULT_REGION=us-east-1 + export AWS_ACCESS_KEY_ID=dummy + export AWS_SECRET_ACCESS_KEY=dummy + + aws dynamodb create-table \ + --table-name Decks \ + --attribute-definitions \ + AttributeName=DeckId,AttributeType=S \ + --key-schema AttributeName=DeckId,KeyType=HASH \ + --endpoint-url http://127.0.0.1:8000 \ + --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 + + aws dynamodb create-table \ + --table-name Slides \ + --attribute-definitions \ + AttributeName=SlideId,AttributeType=S \ + --key-schema AttributeName=SlideId,KeyType=HASH \ + --endpoint-url http://127.0.0.1:8000 \ + --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 + + NIX_REDIRECTS=/etc/protocols=${pkgs.iana-etc}/etc/protocols \ + LD_PRELOAD="${pkgs.libredirect}/lib/libredirect.so" \ + ${handler}/bin/server & + + while ! nc -z 127.0.0.1 8080; do + echo waiting for warp + sleep 1 + done + + echo "Running tests" + ${handler}/bin/test - handler = pkgs.haskellPackagesStatic.deckdeckgo-handler; + touch $out + ''; } diff --git a/infra/handler/Main.hs b/infra/handler/Main.hs deleted file mode 100644 index 27eb06a72..000000000 --- a/infra/handler/Main.hs +++ /dev/null @@ -1,235 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeOperators #-} - -import Control.Monad -import Control.Lens hiding ((.=)) -import Data.Proxy -import Servant.API -import Data.Maybe -import qualified Data.Text as T -import qualified Data.HashMap.Strict as HMS -import UnliftIO -import Data.Aeson ((.=), (.:), (.!=), (.:?)) -import qualified Data.Aeson as Aeson -import qualified Network.AWS as Aws -import qualified Network.AWS.DynamoDB as DynamoDB -import qualified Network.Wai.Handler.Lambda as Lambda -import qualified Network.Wai.Middleware.Cors as Cors -import qualified Servant as Servant -import qualified System.Random as Random - ------------------------------------------------------------------------------- --- API ------------------------------------------------------------------------------- - -data WithId id a = WithId id a - -newtype DeckId = DeckId { unDeckId :: T.Text } - deriving newtype (Aeson.FromJSON, Aeson.ToJSON) - -data Deck = Deck - { deckSlides :: [SlideId] - } - -newtype SlideId = SlideId { unSlideId :: T.Text } - deriving newtype (Aeson.FromJSON, Aeson.ToJSON) - -data Slide = Slide - { slideContent :: T.Text - , slideTemplate :: T.Text - , slideAttributes :: HMS.HashMap T.Text T.Text - } - -instance Aeson.FromJSON Deck where - parseJSON = Aeson.withObject "decK" $ \obj -> - Deck <$> obj .: "deck_slides" - -instance Aeson.FromJSON Slide where - parseJSON = Aeson.withObject "slide" $ \obj -> - Slide <$> - obj .: "slide_content" <*> - obj .: "slide_template" <*> - obj .:? "slide_attributes" .!= HMS.empty - -instance Aeson.ToJSON (WithId DeckId Deck) where - toJSON (WithId deckId deck) = Aeson.object - [ "deck_id" .= deckId - , "deck_slides" .= deckSlides deck - ] - -instance Aeson.ToJSON (WithId SlideId Slide) where - toJSON (WithId slideId slide) = Aeson.object - [ "slide_id" .= slideId - , "slide_template" .= slideTemplate slide - , "slide_attributes" .= slideAttributes slide - , "slide_content" .= slideContent slide - ] - -type API = - "decks" :> DecksAPI :<|> - "slides" :> SlidesAPI - -type DecksAPI = - Get '[JSON] [WithId DeckId Deck] :<|> - ReqBody '[JSON] Deck :> Post '[JSON] (WithId DeckId Deck) - -type SlidesAPI = - Get '[JSON] [WithId SlideId Slide] :<|> - ReqBody '[JSON] Slide :> Post '[JSON] (WithId SlideId Slide) - -api :: Proxy API -api = Proxy - ------------------------------------------------------------------------------- --- SERVER ------------------------------------------------------------------------------- - -main :: IO () -main = do - hSetBuffering stdin LineBuffering - hSetBuffering stdout LineBuffering - - liftIO $ putStrLn "Booting..." - env <- Aws.newEnv Aws.Discover - - liftIO $ putStrLn "Booted!" - Lambda.run $ Cors.simpleCors $ Servant.serve api (server env) - -server :: Aws.Env -> Servant.Server API -server env = serveDecks :<|> serveSlides - where - serveDecks = decksGet env :<|> decksPost env - serveSlides = slidesGet env :<|> slidesPost env - -decksGet :: Aws.Env -> Servant.Handler [WithId DeckId Deck] -decksGet env = do - res <- runAWS env $ Aws.send $ DynamoDB.scan "Decks" - case res of - Right scanResponse -> pure $ catMaybes $ - scanResponse ^. DynamoDB.srsItems <&> itemToDeck - Left e -> do - liftIO $ print e - pure [] - -decksPost :: Aws.Env -> Deck -> Servant.Handler (WithId DeckId Deck) -decksPost env deck = do - - deckId <- liftIO $ DeckId <$> newId - - res <- runAWS env $ Aws.send $ DynamoDB.putItem "Decks" & - DynamoDB.piItem .~ deckToItem deckId deck - - case res of - Right x -> liftIO $ print x - Left e -> liftIO $ print e - - pure $ WithId deckId deck - -runAWS :: MonadIO m => Aws.Env -> Aws.AWS a -> m (Either SomeException a) -runAWS env = - liftIO . - tryAny . - Aws.runResourceT . - Aws.runAWS env . - Aws.within Aws.NorthVirginia - -slidesGet :: Aws.Env -> Servant.Handler [WithId SlideId Slide] -slidesGet env = do - res <- runAWS env $ Aws.send $ DynamoDB.scan "Slides" - case res of - Right scanResponse -> pure $ catMaybes $ - scanResponse ^. DynamoDB.srsItems <&> itemToSlide - Left e -> do - liftIO $ print e - pure [] - -slidesPost :: Aws.Env -> Slide -> Servant.Handler (WithId SlideId Slide) -slidesPost env slide = do - slideId <- liftIO $ SlideId <$> newId - - res <- runAWS env $ - Aws.send $ DynamoDB.putItem "Slides" & - DynamoDB.piItem .~ slideToItem slideId slide - - case res of - Right x -> liftIO $ print x - Left e -> liftIO $ print e - - pure $ WithId slideId slide - -randomString :: Int -> [Char] -> IO String -randomString len allowedChars = - replicateM len $ do - idx <- Random.randomRIO (0, length allowedChars - 1) - pure $ allowedChars !! idx - -randomText :: Int -> [Char] -> IO T.Text -randomText len allowedChars = T.pack <$> randomString len allowedChars - -newId :: IO T.Text -newId = randomText 32 (['0' .. '9'] <> ['a' .. 'z']) - -deckToItem :: DeckId -> Deck -> HMS.HashMap T.Text DynamoDB.AttributeValue -deckToItem deckId Deck{deckSlides} = - HMS.singleton "DeckId" - (DynamoDB.attributeValue & DynamoDB.avS .~ Just (unDeckId deckId)) <> - (if null deckSlides - then HMS.empty - else - HMS.singleton "DeckSlides" - (DynamoDB.attributeValue & DynamoDB.avSS .~ (unSlideId <$> deckSlides)) - ) - -itemToDeck :: HMS.HashMap T.Text DynamoDB.AttributeValue -> Maybe (WithId DeckId Deck) -itemToDeck item = do - deckIdAttr <- HMS.lookup "DeckId" item - deckIdString <- deckIdAttr ^. DynamoDB.avS - deckId <- pure $ DeckId deckIdString - deckSlides <- pure $ case HMS.lookup "DeckSlides" item of - Nothing -> [] - Just slides -> slides ^. DynamoDB.avSS <&> SlideId - pure $ WithId deckId Deck{..} - - -slideToItem :: SlideId -> Slide -> HMS.HashMap T.Text DynamoDB.AttributeValue -slideToItem slideId Slide{slideContent, slideTemplate, slideAttributes} = - HMS.singleton "SlideId" - (DynamoDB.attributeValue & DynamoDB.avS .~ Just (unSlideId slideId)) <> - HMS.singleton "SlideContent" - (DynamoDB.attributeValue & DynamoDB.avS .~ Just slideContent) <> - HMS.singleton "SlideTemplate" - (DynamoDB.attributeValue & DynamoDB.avS .~ Just slideTemplate) <> - (if HMS.null slideAttributes - then HMS.empty - else - HMS.singleton "SlideAttributes" - (DynamoDB.attributeValue & DynamoDB.avM .~ ( - (\txt -> DynamoDB.attributeValue & DynamoDB.avS .~ Just txt) <$> - slideAttributes - )) - ) - -itemToSlide :: HMS.HashMap T.Text DynamoDB.AttributeValue -> Maybe (WithId SlideId Slide) -itemToSlide item = do - slideIdAttr <- HMS.lookup "SlideId" item - slideIdString <- slideIdAttr ^. DynamoDB.avS - slideId <- pure $ SlideId slideIdString - - slideContentAttr <- HMS.lookup "SlideContent" item - slideContent <- slideContentAttr ^. DynamoDB.avS - - slideTemplateAttr <- HMS.lookup "SlideTemplate" item - slideTemplate <- slideTemplateAttr ^. DynamoDB.avS - - slideAttributesAttr <- HMS.lookup "SlideAttributes" item - slideAttributes <- pure $ slideAttributesAttr ^. DynamoDB.avM & - HMS.mapMaybe (\attr -> attr ^. DynamoDB.avS) - - pure $ WithId slideId Slide{..} diff --git a/infra/handler/app/Handler.hs b/infra/handler/app/Handler.hs new file mode 100644 index 000000000..ced7cac14 --- /dev/null +++ b/infra/handler/app/Handler.hs @@ -0,0 +1,16 @@ +import UnliftIO +import qualified Network.AWS as Aws +import qualified DeckGo.Handler +import qualified Network.Wai.Handler.Lambda as Lambda +import qualified Network.Wai.Middleware.Cors as Cors + +main :: IO () +main = do + hSetBuffering stdin LineBuffering + hSetBuffering stdout LineBuffering + + liftIO $ putStrLn "Booting..." + env <- Aws.newEnv Aws.Discover + + liftIO $ putStrLn "Booted!" + Lambda.run $ Cors.simpleCors $ DeckGo.Handler.application env diff --git a/infra/handler/app/Server.hs b/infra/handler/app/Server.hs new file mode 100644 index 000000000..28df1de2b --- /dev/null +++ b/infra/handler/app/Server.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE OverloadedStrings #-} + +import UnliftIO +import Control.Lens +import qualified Network.HTTP.Client as HTTPClient +import qualified Network.HTTP.Client.TLS as HTTPClient +import qualified Network.AWS as Aws +import qualified DeckGo.Handler +import qualified Network.Wai.Handler.Warp as Warp + +main :: IO () +main = do + hSetBuffering stdin LineBuffering + hSetBuffering stdout LineBuffering + mgr <- HTTPClient.newManager HTTPClient.tlsManagerSettings + { HTTPClient.managerModifyRequest = rerouteDynamoDB + } + env <- Aws.newEnv Aws.Discover <&> Aws.envManager .~ mgr + Warp.run 8080 $ DeckGo.Handler.application env + +rerouteDynamoDB :: HTTPClient.Request -> IO HTTPClient.Request +rerouteDynamoDB req = + case HTTPClient.host req of + "dynamodb.us-east-1.amazonaws.com" -> + pure req + { HTTPClient.host = "127.0.0.1" + , HTTPClient.port = 8000 + , HTTPClient.secure = False + } + _ -> pure req diff --git a/infra/handler/app/Test.hs b/infra/handler/app/Test.hs new file mode 100644 index 000000000..e8513003b --- /dev/null +++ b/infra/handler/app/Test.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE LambdaCase #-} + +import Network.HTTP.Client (newManager, defaultManagerSettings) +import Servant.API +import Servant.Client +import DeckGo.Handler +import qualified Data.HashMap.Strict as HMS + +main :: IO () +main = do + manager' <- newManager defaultManagerSettings + + let clientEnv = mkClientEnv manager' (BaseUrl Http "localhost" 8080 "") + + runClientM decksGet' clientEnv >>= \case + Left err -> error $ "Expected decks, got error: " <> show err + Right [] -> pure () + Right decks -> error $ "Expected 0 decks, got: " <> show decks + + let someDeck = Deck { deckSlides = [] } + + deckId <- runClientM (decksPost' someDeck) clientEnv >>= \case + Left err -> error $ "Expected new deck, got error: " <> show err + Right (WithId deckId _) -> pure deckId + + let someSlide = Slide "foo" "bar" HMS.empty + + slideId <- runClientM (slidesPost' someSlide) clientEnv >>= \case + Left err -> error $ "Expected new slide, got error: " <> show err + Right (WithId slideId _) -> pure slideId + + let newDeck = Deck { deckSlides = [ slideId ] } + + runClientM (decksPut' deckId newDeck) clientEnv >>= \case + Left err -> error $ "Expected updated deck, got error: " <> show err + Right {} -> pure () + + runClientM decksGet' clientEnv >>= \case + Left err -> error $ "Expected decks, got error: " <> show err + Right decks -> + if decks == [WithId deckId newDeck] then pure () else (error $ "Expected updated decks, got: " <> show decks) + + runClientM slidesGet' clientEnv >>= \case + Left err -> error $ "Expected slides, got error: " <> show err + Right slides -> + if slides == [WithId slideId someSlide] then pure () else (error $ "Expected slides, got: " <> show slides) + + let updatedSlide = Slide "foo" "quux" HMS.empty + + runClientM (slidesPut' slideId updatedSlide) clientEnv >>= \case + Left err -> error $ "Expected new slide, got error: " <> show err + Right {} -> pure () + + runClientM slidesGet' clientEnv >>= \case + Left err -> error $ "Expected updated slides, got error: " <> show err + Right slides -> + if slides == [WithId slideId updatedSlide] then pure () else (error $ "Expected updated slides, got: " <> show slides) + + runClientM (slidesDelete' slideId) clientEnv >>= \case + Left err -> error $ "Expected slide delete, got error: " <> show err + Right {} -> pure () + + runClientM slidesGet' clientEnv >>= \case + Left err -> error $ "Expected no slides, got error: " <> show err + Right slides -> + if slides == [] then pure () else (error $ "Expected no slides, got: " <> show slides) + + runClientM (decksDelete' deckId) clientEnv >>= \case + Left err -> error $ "Expected deck delete, got error: " <> show err + Right {} -> pure () + + runClientM decksGet' clientEnv >>= \case + Left err -> error $ "Expected no decks, got error: " <> show err + Right decks -> + if decks == [] then pure () else (error $ "Expected no decks, got: " <> show decks) + +-- 'client' allows you to produce operations to query an API from a client. +decksGet' :: ClientM [WithId DeckId Deck] +decksPost' :: Deck -> ClientM (WithId DeckId Deck) +decksPut' :: DeckId -> Deck -> ClientM (WithId DeckId Deck) +decksDelete' :: DeckId -> ClientM () +slidesGet' :: ClientM [WithId SlideId Slide] +slidesPost' :: Slide -> ClientM (WithId SlideId Slide) +slidesPut' :: SlideId -> Slide -> ClientM (WithId SlideId Slide) +slidesDelete' :: SlideId -> ClientM () +((decksGet' :<|> decksPost' :<|> decksPut' :<|> decksDelete') :<|> + (slidesGet' :<|> slidesPost' :<|> slidesPut' :<|> slidesDelete') + ) = client api diff --git a/infra/handler/package.yaml b/infra/handler/package.yaml index 7204f12e7..71eefaae2 100644 --- a/infra/handler/package.yaml +++ b/infra/handler/package.yaml @@ -15,11 +15,36 @@ dependencies: - text - unliftio - unordered-containers + - wai - wai-cors - wai-lambda ghc-options: - -Wall -executable: - main: Main.hs +library: + source-dirs: src + +executables: + + # The AWS Lambda handler + handler: + main: app/Handler.hs + dependencies: + - deckdeckgo-handler + + server: + main: app/Server.hs + dependencies: + - deckdeckgo-handler + - warp + - http-client + - http-client-tls + - lens + + test: + main: app/Test.hs + dependencies: + - deckdeckgo-handler + - http-client + - servant-client diff --git a/infra/handler/src/DeckGo/Handler.hs b/infra/handler/src/DeckGo/Handler.hs new file mode 100644 index 000000000..8e132e5c4 --- /dev/null +++ b/infra/handler/src/DeckGo/Handler.hs @@ -0,0 +1,384 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeOperators #-} + +module DeckGo.Handler where + +import Control.Monad +import Control.Lens hiding ((.=)) +import Data.Proxy +import Servant.API +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.HashMap.Strict as HMS +import UnliftIO +import Data.Aeson ((.=), (.:), (.!=), (.:?)) +import qualified Data.Aeson as Aeson +import qualified Network.AWS as Aws +import qualified Network.AWS.DynamoDB as DynamoDB +import qualified Network.Wai as Wai +import qualified Servant as Servant +import qualified System.Random as Random + +------------------------------------------------------------------------------ +-- API +------------------------------------------------------------------------------ + +data WithId id a = WithId id a + deriving (Show, Eq) + +newtype DeckId = DeckId { unDeckId :: T.Text } + deriving newtype (Aeson.FromJSON, Aeson.ToJSON, FromHttpApiData, ToHttpApiData, Show, Eq) + +data Deck = Deck + { deckSlides :: [SlideId] + } deriving (Show, Eq) + +newtype SlideId = SlideId { unSlideId :: T.Text } + deriving newtype (Aeson.FromJSON, Aeson.ToJSON, FromHttpApiData, ToHttpApiData, Show, Eq) + +data Slide = Slide + { slideContent :: T.Text + , slideTemplate :: T.Text + , slideAttributes :: HMS.HashMap T.Text T.Text + } deriving (Show, Eq) + +instance Aeson.FromJSON Deck where + parseJSON = Aeson.withObject "decK" $ \obj -> + Deck <$> obj .: "deck_slides" + +instance Aeson.ToJSON Deck where + toJSON deck = Aeson.object + [ "deck_slides" .= deckSlides deck + ] + +instance Aeson.FromJSON Slide where + parseJSON = Aeson.withObject "slide" $ \obj -> + Slide <$> + obj .: "slide_content" <*> + obj .: "slide_template" <*> + obj .:? "slide_attributes" .!= HMS.empty + +instance Aeson.ToJSON Slide where + toJSON slide = Aeson.object + [ "slide_template" .= slideTemplate slide + , "slide_attributes" .= slideAttributes slide + , "slide_content" .= slideContent slide + ] + +instance Aeson.FromJSON (WithId DeckId Deck) where + parseJSON = Aeson.withObject "WithId DeckId Deck" $ \o -> + WithId <$> + (DeckId <$> o .: "deck_id") <*> + (Deck <$> o .: "deck_slides") + +instance Aeson.ToJSON (WithId DeckId Deck) where + toJSON (WithId deckId deck) = Aeson.object + [ "deck_id" .= deckId + , "deck_slides" .= deckSlides deck + ] + +instance Aeson.FromJSON (WithId SlideId Slide) where + parseJSON = Aeson.withObject "WithId SlideId Slide" $ \o -> + WithId <$> + (SlideId <$> o .: "slide_id") <*> + (Slide <$> + o .: "slide_content" <*> + o .: "slide_template" <*> + o .: "slide_attributes" + ) + +instance Aeson.ToJSON (WithId SlideId Slide) where + toJSON (WithId slideId slide) = Aeson.object + [ "slide_id" .= slideId + , "slide_template" .= slideTemplate slide + , "slide_attributes" .= slideAttributes slide + , "slide_content" .= slideContent slide + ] + +type API = + "decks" :> DecksAPI :<|> + "slides" :> SlidesAPI + +type DecksAPI = + Get '[JSON] [WithId DeckId Deck] :<|> + ReqBody '[JSON] Deck :> Post '[JSON] (WithId DeckId Deck) :<|> + Capture "deck_id" DeckId :> ReqBody '[JSON] Deck :> Put '[JSON] (WithId DeckId Deck) :<|> + Capture "deck_id" DeckId :> Delete '[JSON] () + +type SlidesAPI = + Get '[JSON] [WithId SlideId Slide] :<|> + ReqBody '[JSON] Slide :> Post '[JSON] (WithId SlideId Slide) :<|> + Capture "slide_id" SlideId :> ReqBody '[JSON] Slide :> Put '[JSON] (WithId SlideId Slide) :<|> + Capture "slide_id" SlideId :> Delete '[JSON] () + +api :: Proxy API +api = Proxy + +------------------------------------------------------------------------------ +-- SERVER +------------------------------------------------------------------------------ + +application :: Aws.Env -> Wai.Application +application env = Servant.serve api (server env) + +server :: Aws.Env -> Servant.Server API +server env = serveDecks :<|> serveSlides + where + serveDecks = + decksGet env :<|> + decksPost env :<|> + decksPut env :<|> + decksDelete env + serveSlides = + slidesGet env :<|> + slidesPost env :<|> + slidesPut env :<|> + slidesDelete env + +decksGet :: Aws.Env -> Servant.Handler [WithId DeckId Deck] +decksGet env = do + res <- runAWS env $ Aws.send $ DynamoDB.scan "Decks" + case res of + Right scanResponse -> + case sequence $ scanResponse ^. DynamoDB.srsItems <&> itemToDeck of + Nothing -> do + liftIO $ putStrLn $ "Could not parse response: " <> show scanResponse + Servant.throwError Servant.err500 + Just ids -> pure ids + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + +decksPost :: Aws.Env -> Deck -> Servant.Handler (WithId DeckId Deck) +decksPost env deck = do + + deckId <- liftIO $ DeckId <$> newId + + res <- runAWS env $ Aws.send $ DynamoDB.putItem "Decks" & + DynamoDB.piItem .~ deckToItem deckId deck + + case res of + Right {} -> pure () + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + + pure $ WithId deckId deck + +decksPut :: Aws.Env -> DeckId -> Deck -> Servant.Handler (WithId DeckId Deck) +decksPut env deckId deck = do + + res <- runAWS env $ Aws.send $ DynamoDB.updateItem "Decks" & + DynamoDB.uiUpdateExpression .~ Just "SET DeckSlides = :s" & + DynamoDB.uiExpressionAttributeValues .~ deckToItem' deck & + DynamoDB.uiReturnValues .~ Just DynamoDB.UpdatedNew & + DynamoDB.uiKey .~ HMS.singleton "DeckId" + (deckIdToAttributeValue deckId) + + case res of + Right {} -> pure () + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + + pure $ WithId deckId deck + +decksDelete :: Aws.Env -> DeckId -> Servant.Handler () +decksDelete env deckId = do + + res <- runAWS env $ Aws.send $ DynamoDB.deleteItem "Decks" & + DynamoDB.diKey .~ HMS.singleton "DeckId" + (deckIdToAttributeValue deckId) + + case res of + Right {} -> pure () + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + +runAWS :: MonadIO m => Aws.Env -> Aws.AWS a -> m (Either SomeException a) +runAWS env = + liftIO . + tryAny . + Aws.runResourceT . + Aws.runAWS env . + Aws.within Aws.NorthVirginia + +slidesGet :: Aws.Env -> Servant.Handler [WithId SlideId Slide] +slidesGet env = do + res <- runAWS env $ Aws.send $ DynamoDB.scan "Slides" + case res of + Right scanResponse -> + case sequence $ scanResponse ^. DynamoDB.srsItems <&> itemToSlide of + Nothing -> do + liftIO $ putStrLn $ "Could not parse respose: " <> show scanResponse + Servant.throwError Servant.err500 + Just ids -> pure ids + + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + +slidesPost :: Aws.Env -> Slide -> Servant.Handler (WithId SlideId Slide) +slidesPost env slide = do + slideId <- liftIO $ SlideId <$> newId + + res <- runAWS env $ + Aws.send $ DynamoDB.putItem "Slides" & + DynamoDB.piItem .~ slideToItem slideId slide + + case res of + Right {} -> pure () + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + + pure $ WithId slideId slide + +slidesPut :: Aws.Env -> SlideId -> Slide -> Servant.Handler (WithId SlideId Slide) +slidesPut env slideId slide = do + + res <- runAWS env $ Aws.send $ DynamoDB.updateItem "Slides" & + DynamoDB.uiUpdateExpression .~ Just + "SET SlideContent = :c, SlideTemplate = :t, SlideAttributes = :a" & + DynamoDB.uiExpressionAttributeValues .~ slideToItem' slide & + DynamoDB.uiReturnValues .~ Just DynamoDB.UpdatedNew & + DynamoDB.uiKey .~ HMS.singleton "SlideId" + (slideIdToAttributeValue slideId) + + case res of + Right x -> liftIO $ print x + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + + pure $ WithId slideId slide + +slidesDelete :: Aws.Env -> SlideId -> Servant.Handler () +slidesDelete env slideId = do + + res <- runAWS env $ Aws.send $ DynamoDB.deleteItem "Slides" & + DynamoDB.diKey .~ HMS.singleton "SlideId" + (slideIdToAttributeValue slideId) + + case res of + Right x -> liftIO $ print x + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + +randomString :: Int -> [Char] -> IO String +randomString len allowedChars = + replicateM len $ do + idx <- Random.randomRIO (0, length allowedChars - 1) + pure $ allowedChars !! idx + +randomText :: Int -> [Char] -> IO T.Text +randomText len allowedChars = T.pack <$> randomString len allowedChars + +newId :: IO T.Text +newId = randomText 32 (['0' .. '9'] <> ['a' .. 'z']) + +deckIdToAttributeValue :: DeckId -> DynamoDB.AttributeValue +deckIdToAttributeValue (DeckId deckId) = + DynamoDB.attributeValue & DynamoDB.avS .~ Just deckId + +deckIdFromAttributeValue :: DynamoDB.AttributeValue -> Maybe DeckId +deckIdFromAttributeValue attr = DeckId <$> attr ^. DynamoDB.avS + +deckSlidesToAttributeValue :: [SlideId] -> DynamoDB.AttributeValue +deckSlidesToAttributeValue deckSlides = + DynamoDB.attributeValue & DynamoDB.avL .~ + (slideIdToAttributeValue <$> deckSlides) + +deckSlidesFromAttributeValue :: DynamoDB.AttributeValue -> Maybe [SlideId] +deckSlidesFromAttributeValue attr = + traverse slideIdFromAttributeValue (attr ^. DynamoDB.avL) + +slideIdToAttributeValue :: SlideId -> DynamoDB.AttributeValue +slideIdToAttributeValue (SlideId slideId) = + DynamoDB.attributeValue & DynamoDB.avS .~ Just slideId + +slideIdFromAttributeValue :: DynamoDB.AttributeValue -> Maybe SlideId +slideIdFromAttributeValue attr = SlideId <$> attr ^. DynamoDB.avS + +deckToItem :: DeckId -> Deck -> HMS.HashMap T.Text DynamoDB.AttributeValue +deckToItem deckId Deck{deckSlides} = + HMS.singleton "DeckId" (deckIdToAttributeValue deckId) <> + HMS.singleton "DeckSlides" (deckSlidesToAttributeValue deckSlides) + +deckToItem' :: Deck -> HMS.HashMap T.Text DynamoDB.AttributeValue +deckToItem' Deck{deckSlides} = + HMS.singleton ":s" (deckSlidesToAttributeValue deckSlides) + +itemToDeck :: HMS.HashMap T.Text DynamoDB.AttributeValue -> Maybe (WithId DeckId Deck) +itemToDeck item = do + deckId <- HMS.lookup "DeckId" item >>= deckIdFromAttributeValue + deckSlides <- HMS.lookup "DeckSlides" item >>= deckSlidesFromAttributeValue + pure $ WithId deckId Deck{..} + +slideToItem :: SlideId -> Slide -> HMS.HashMap T.Text DynamoDB.AttributeValue +slideToItem slideId Slide{slideContent, slideTemplate, slideAttributes} = + HMS.singleton "SlideId" (slideIdToAttributeValue slideId) <> + HMS.singleton "SlideContent" (slideContentToAttributeValue slideContent) <> + HMS.singleton "SlideTemplate" (slideTemplateToAttributeValue slideTemplate) <> + HMS.singleton "SlideAttributes" (slideAttributesToAttributeValue slideAttributes) + +slideContentToAttributeValue :: T.Text -> DynamoDB.AttributeValue +slideContentToAttributeValue content = + DynamoDB.attributeValue & DynamoDB.avB .~ Just (T.encodeUtf8 content) + +slideContentFromAttributeValue :: DynamoDB.AttributeValue -> Maybe T.Text +slideContentFromAttributeValue attr = toSlideContent <$> attr ^. DynamoDB.avB + where + toSlideContent = T.decodeUtf8 + +slideTemplateToAttributeValue :: T.Text -> DynamoDB.AttributeValue +slideTemplateToAttributeValue content = + DynamoDB.attributeValue & DynamoDB.avB .~ Just (T.encodeUtf8 content) + +slideTemplateFromAttributeValue :: DynamoDB.AttributeValue -> Maybe T.Text +slideTemplateFromAttributeValue attr = toSlideTemplate <$> attr ^. DynamoDB.avB + where + toSlideTemplate = T.decodeUtf8 + +slideAttributesToAttributeValue :: HMS.HashMap T.Text T.Text -> DynamoDB.AttributeValue +slideAttributesToAttributeValue attributes = + DynamoDB.attributeValue & DynamoDB.avM .~ + HMS.map attributeValueToAttributeValue attributes + where + attributeValueToAttributeValue :: T.Text -> DynamoDB.AttributeValue + attributeValueToAttributeValue attrValue = + DynamoDB.attributeValue & DynamoDB.avB .~ Just (T.encodeUtf8 attrValue) + +slideAttributesFromAttributeValue :: DynamoDB.AttributeValue -> Maybe (HMS.HashMap T.Text T.Text) +slideAttributesFromAttributeValue attr = + traverse attributeValueFromAttributeValue (attr ^. DynamoDB.avM) + where + attributeValueFromAttributeValue :: DynamoDB.AttributeValue -> Maybe T.Text + attributeValueFromAttributeValue attrValue = + T.decodeUtf8 <$> attrValue ^. DynamoDB.avB + +slideToItem' :: Slide -> HMS.HashMap T.Text DynamoDB.AttributeValue +slideToItem' Slide{slideContent, slideTemplate, slideAttributes} = + HMS.singleton ":c" (slideContentToAttributeValue slideContent) <> + HMS.singleton ":t" (slideTemplateToAttributeValue slideTemplate) <> + HMS.singleton ":a" (slideAttributesToAttributeValue slideAttributes) + +itemToSlide :: HMS.HashMap T.Text DynamoDB.AttributeValue -> Maybe (WithId SlideId Slide) +itemToSlide item = do + slideId <- HMS.lookup "SlideId" item >>= slideIdFromAttributeValue + + slideContent <- HMS.lookup "SlideContent" item >>= slideContentFromAttributeValue + + slideTemplate <- HMS.lookup "SlideTemplate" item >>= slideTemplateFromAttributeValue + slideAttributes <- HMS.lookup "SlideAttributes" item >>= slideAttributesFromAttributeValue + + pure $ WithId slideId Slide{..} diff --git a/infra/nix/default.nix b/infra/nix/default.nix index 4c224aaf4..475a89837 100644 --- a/infra/nix/default.nix +++ b/infra/nix/default.nix @@ -3,6 +3,7 @@ with rec { sources = import ./sources.nix; pkgs = import sources.nixpkgs {}; wai-lambda = pkgs.callPackage "${sources.wai-lambda}/nix/packages.nix" {}; + surveyor = pkgs.callPackage ../surveyor {}; pkgsStatic = (import "${sources.static-haskell-nix}/survey" @@ -41,5 +42,6 @@ with rec pkgs // { inherit haskellPackagesStatic haskellPackages sources wai-lambda; + inherit (surveyor) surveyor; inherit (import sources.niv {}) niv; } diff --git a/infra/nix/sources.json b/infra/nix/sources.json index 730d50aa2..04a581533 100644 --- a/infra/nix/sources.json +++ b/infra/nix/sources.json @@ -20,6 +20,12 @@ "description": "Nixpkgs/NixOS branches that track the Nixpkgs/NixOS channels", "rev": "88ae8f7d55efa457c95187011eb410d097108445" }, + "dynamodb": { + "url": "https://s3.eu-central-1.amazonaws.com/dynamodb-local-frankfurt/dynamodb_local_latest.tar.gz", + "url_template": "https://github.com///archive/.tar.gz", + "type": "file", + "sha256": "0hrwxg4igyll40y7l1s0icg55g247fl8cjs4rrcpjf8d7m0bb09j" + }, "static-haskell-nix": { "homepage": "", "url": "https://github.com/nh2/static-haskell-nix/archive/9781df8a48eade302d159ce63a7ab0c30247788c.tar.gz", @@ -33,13 +39,14 @@ }, "niv": { "homepage": "https://github.com/nmattia/niv", - "url": "https://github.com/nmattia/niv/archive/7f72d723d00fcc3f177138acbfcf9d581beee9e1.tar.gz", + "url": "https://github.com/nmattia/niv/archive/c2698b0780b783880e0b1a520723948fe3b5c26a.tar.gz", "owner": "nmattia", "branch": "master", "url_template": "https://github.com///archive/.tar.gz", "repo": "niv", - "sha256": "0177xhz55519xrak1fmv3gilbg8330lmbfkbizkc8zgp58skcmjw", + "type": "tarball", + "sha256": "0v68x0h9si6kjqg5fcjrgsbsf4x18m32a786yvjmrdkrki9qwmcq", "description": "Easy dependency management for Nix projects", - "rev": "7f72d723d00fcc3f177138acbfcf9d581beee9e1" + "rev": "c2698b0780b783880e0b1a520723948fe3b5c26a" } } \ No newline at end of file diff --git a/infra/nix/sources.nix b/infra/nix/sources.nix index 30b77ce5f..4fecc49e9 100644 --- a/infra/nix/sources.nix +++ b/infra/nix/sources.nix @@ -1,26 +1,35 @@ +# This file has been generated by Niv. + # A record, from name to path, of the third-party packages -with +with rec { - versions = builtins.fromJSON (builtins.readFile ./sources.json); + pkgs = import {}; - # fetchTarball version that is compatible between all the versions of Nix - fetchTarball = - { url, sha256 }: - if builtins.lessThan builtins.nixVersion "1.12" then - builtins.fetchTarball { inherit url; } - else - builtins.fetchTarball { inherit url sha256; }; -}; + sources = builtins.fromJSON (builtins.readFile ./sources.json); + mapAttrs = builtins.mapAttrs or + (f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))); + + getFetcher = spec: + let fetcherName = + if builtins.hasAttr "type" spec + then builtins.getAttr "type" spec + else "tarball"; + in builtins.getAttr fetcherName { + "tarball" = pkgs.fetchzip; + "file" = pkgs.fetchurl; + }; +}; # NOTE: spec must _not_ have an "outPath" attribute -builtins.mapAttrs (_: spec: +mapAttrs (_: spec: if builtins.hasAttr "outPath" spec then abort - "The values in versions.json should not have an 'outPath' attribute" + "The values in sources.json should not have an 'outPath' attribute" else if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec then spec // - { outPath = fetchTarball { inherit (spec) url sha256; } ; } + { outPath = getFetcher spec { inherit (spec) url sha256; } ; } else spec - ) versions + ) sources diff --git a/infra/shell.nix b/infra/shell.nix index afc0e2352..93f5d979c 100644 --- a/infra/shell.nix +++ b/infra/shell.nix @@ -1,6 +1,10 @@ with { pkgs = import ./nix {}; }; -pkgs.mkShell - { buildInputs = with pkgs; [ niv terraform awscli ]; - } +let + pkg = pkgs.haskellPackages.developPackage + { root = ./handler; }; +in + pkg.overrideAttrs(attr: { + buildInputs = with pkgs; [ niv terraform awscli ]; + })