packages feed

genesis (empty) → 0.0.1.0

raw patch · 10 files changed

+410/−0 lines, 10 filesdep +basedep +directorydep +envparsesetup-changed

Dependencies added: base, directory, envparse, file-embed, filepath, genesis, hspec, monad-control, monad-logger, monad-persist, persistent, persistent-postgresql, persistent-sqlite, persistent-template, resource-pool, template-haskell, text, text-conversions

Files

+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## 0.0.1.0 (April 24th, 2017)++- Initial release
+ Setup.hs view
@@ -0,0 +1,7 @@+-- This script is used to build and install your package. Typically you don't+-- need to change it. The Cabal documentation has more information about this+-- file: <https://www.haskell.org/cabal/users-guide/installing-packages.html>.+import qualified Distribution.Simple++main :: IO ()+main = Distribution.Simple.defaultMain
+ genesis.cabal view
@@ -0,0 +1,73 @@+-- This file has been generated from package.yaml by hpack version 0.15.0.+--+-- see: https://github.com/sol/hpack++name:           genesis+version:        0.0.1.0+synopsis:       Opinionated bootstrapping for Haskell web services.+description:    Opinionated bootstrapping for Haskell web services.+category:       Other+homepage:       https://github.com/cjdev/genesis#readme+bug-reports:    https://github.com/cjdev/genesis/issues+maintainer:     Alexis King <lexi.lambda@gmail.com>+copyright:      2017 CJ Affiliate by Conversant+license:        ISC+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    CHANGELOG.md+    package.yaml+    stack.yaml++source-repository head+  type: git+  location: https://github.com/cjdev/genesis++library+  hs-source-dirs:+      library+  default-extensions: ConstraintKinds DeriveGeneric DuplicateRecordFields FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns OverloadedStrings RankNTypes ScopedTypeVariables TypeApplications TypeOperators+  ghc-options: -Wall+  build-depends:+      base                   >= 4.9.0.0 && < 5+    , directory              >= 1.2.7.0+    , envparse               >= 0.3.0 && < 1+    , file-embed             >= 0.0.10 && < 1+    , filepath+    , monad-control          >= 1.0.0.0 && < 2+    , monad-logger           >= 0.3.10+    , monad-persist          >= 0.0.1.0 && < 1+    , persistent             >= 2.5 && < 3+    , persistent-postgresql  >= 2.5 && < 3+    , persistent-template    >= 2.5 && < 3+    , resource-pool+    , template-haskell       >= 2.11.0.0 && < 2.12+    , text+    , text-conversions       >= 0.2.0 && < 1+  exposed-modules:+      Genesis.Persist+      Genesis.Persist.Base+      Genesis.Persist.Migrate+  default-language: Haskell2010++test-suite genesis-test-suite+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+      test-suite+  default-extensions: ConstraintKinds DeriveGeneric DuplicateRecordFields FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns OverloadedStrings RankNTypes ScopedTypeVariables TypeApplications TypeOperators+  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N+  build-depends:+      base+    , genesis+    , hspec+    , monad-control+    , monad-logger+    , monad-persist+    , persistent-sqlite+    , persistent-template+    , text+  other-modules:+      Genesis.MigrateSpec+  default-language: Haskell2010
+ library/Genesis/Persist.hs view
@@ -0,0 +1,16 @@+module Genesis.Persist+  ( -- * Connecting to PostgreSQL+    -- | The bindings in this section are re-exported from+    -- "Genesis.Persist.Base".+    PostgresOptions(..)+  , postgresOptions+  , withPostgresqlPool+  , withPostgresqlConn+    -- * Compiling and running SQL migrations+    -- | The bindings in this section are re-exported from+    -- "Genesis.Persist.Migrate".+  , runMigrations+  ) where++import Genesis.Persist.Base+import Genesis.Persist.Migrate
+ library/Genesis/Persist/Base.hs view
@@ -0,0 +1,77 @@+{-# OPTIONS_HADDOCK not-home #-}+{-# LANGUAGE NoDuplicateRecordFields #-}++{-|+  This module provides utilities for using PostgreSQL with+  "Control.Monad.Persist" and @persistent@.+-}+module Genesis.Persist.Base+  ( PostgresOptions(..)+  , postgresOptions+  , withPostgresqlPool+  , withPostgresqlConn+  ) where++import qualified Database.Persist.Postgresql as PG+import qualified Env++import Control.Monad ((<=<))+import Control.Monad.Logger (MonadLogger)+import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Monad.IO.Class (MonadIO)+import Control.Monad.Persist (SqlBackend)+import Data.Monoid ((<>))+import Data.Pool (Pool)+import Data.Text (Text)+import Data.Text.Conversions (fromText, toText, unUTF8)+import GHC.Generics (Generic)++{-|+  Connection options needed to connect to PostgreSQL. You can use+  'postgresOptions' to parse these options from the environment.+-}+data PostgresOptions = PostgresOptions+  { host :: Text+  , port :: Int+  , user :: Text+  , dbName :: Text+  , password :: Text+  } deriving (Eq, Show, Generic)++{-|+  An @envparse@ 'Env.Parser' that parses 'PostgresOptions' from the environment,+  looking for the environment variables @PG_HOST@, @PG_PORT@, @PG_USER@,+  @PG_USER@, @PG_DB_NAME@, and @PG_PASSWORD@. All of them are optional except+  for @PG_DB_NAME@.+-}+postgresOptions :: (Env.AsEmpty e, Env.AsUnread e, Env.AsUnset e) => Env.Parser e PostgresOptions+postgresOptions = PostgresOptions+    <$> Env.var (Env.str <=< Env.nonempty) "PG_HOST" (helpDef "localhost" "host of postgres database")+    <*> Env.var (Env.auto <=< Env.nonempty) "PG_PORT" (helpDef 5432 "port of postgres database")+    <*> Env.var (Env.str <=< Env.nonempty) "PG_USER" (helpDef "postgres" "user to connect to postgres as")+    <*> Env.var (Env.str <=< Env.nonempty) "PG_DB_NAME" (Env.help "postgres database name to connect to")+    <*> Env.var Env.str "PG_PASSWORD" (helpDef "" "password to connect to postgres with")+  where helpDef def msg = Env.def def <> Env.help (msg ++ " (default: " ++ show def ++ ")")++{-|+  Like 'PG.withPostgresqlPool' from "Database.Persist.Postgresql", except using+  'PostgresOptions'.+-}+withPostgresqlPool :: (MonadBaseControl IO m, MonadLogger m, MonadIO m)+                   => PostgresOptions -- ^ Options to connect to the database.+                   -> Int -- ^ Number of connections to be kept open in the pool.+                   -> (Pool SqlBackend -> m a) -- ^ Action to be executed that uses the connection pool.+                   -> m a+withPostgresqlPool opts = PG.withPostgresqlPool (pgConnString opts)++{-|+Like 'PG.withPostgresqlConn' from "Database.Persist.Postgresql", except using+'PostgresOptions'.+-}+withPostgresqlConn :: (MonadBaseControl IO m, MonadLogger m, MonadIO m) => PostgresOptions -> (SqlBackend -> m a) -> m a+withPostgresqlConn opts = PG.withPostgresqlConn (pgConnString opts)++pgConnString :: PostgresOptions -> PG.ConnectionString+pgConnString PostgresOptions { host, port, user, dbName, password } =+  unUTF8 . fromText $ "host=" <> host <> " port=" <> toText (show port) <> " user=" <> user+                   <> " dbname=" <> dbName <> " password=" <> password
+ library/Genesis/Persist/Migrate.hs view
@@ -0,0 +1,102 @@+{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}+{-# OPTIONS_HADDOCK not-home #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}++{-|+  This module provides functions for creating and running migrations written as+  plain SQL files. By using 'runMigrations', the migrations will be compiled+  directly into your Haskell application using Template Haskell, so the files+  will not need to be present in the eventual runtime environment.++  For more information, see the documentation for 'runMigrations'.+-}+module Genesis.Persist.Migrate (runMigrations, runMigrations') where++import qualified Data.Text.IO as T++import Control.Monad (filterM, forM, forM_, unless, when)+import Control.Monad.Logger (MonadLogger, logDebugNS, logInfoNS)+import Control.Monad.Persist (MonadSqlPersist, insert_, rawExecute, runMigrationSilent, selectList, transactionSave)+import System.Directory (doesDirectoryExist, doesFileExist, doesPathExist, listDirectory)+import System.FilePath ((</>))+import Data.FileEmbed (makeRelativeToProject)+import Data.List (isSuffixOf, sort)+import Data.Monoid ((<>))+import Data.Text (Text)+import Data.Text.Conversions (toText)+import Database.Persist.Sql (Entity(..))+import Database.Persist.TH (share, mkPersist, sqlSettings, mkMigrate, persistLowerCase)+import Language.Haskell.TH+import Language.Haskell.TH.Syntax++share [mkPersist sqlSettings, mkMigrate "migrateSchema"] [persistLowerCase|+SchemaMigration+  name FilePath+  deriving Eq Show+|]++{-|+  Compiles a set of .sql files in a particular directory into your application,+  then runs them against a database at runtime. This function is implemented+  with Template Haskell so that it can read the migration files at compile-time,+  but it semantically has the type+  @('MonadLogger' m, 'MonadSqlPersist' m) => 'FilePath' -> m ()@. Migrations+  will be executed in order based on their filename, according to 'sort'.++  The 'FilePath' provided should be relative to your /project root/, and it will+  detect all files within the immediate directory (that is, /not/ in+  subdirectories) with the suffix @.sql@.++  Example:++  @+  main :: IO ()+  main = 'Control.Monad.Logger.runStderrLoggingT' $ withSqliteConn ":memory:" ('Control.Monad.Persist.runSqlPersistT' $('runMigrations' "db/migrations"))+  @+-}+runMigrations :: FilePath -> Q Exp+runMigrations dir = do+  migrationsDir <- makeRelativeToProject dir+  migrationsDirExists <- runIO $ doesPathExist migrationsDir+  unless migrationsDirExists $+    fail $ "No such directory ‘" ++ dir ++ "’ exists at root of project.\n        (Looking at ‘" ++ migrationsDir ++ "’)."++  migrationsDirIsDir <- runIO $ doesDirectoryExist migrationsDir+  unless migrationsDirIsDir $+    fail $ "The file at ‘" ++ dir ++ "’ in root of project is not a directory.\n        (Looking at ‘" ++ migrationsDir ++ "’)."++  allFiles <- runIO $ listDirectory migrationsDir+  nonDirectoryFiles <- runIO $ filterM (doesFileExist . (migrationsDir </>)) allFiles+  let migrationFiles = sort $ filter (isSuffixOf ".sql") nonDirectoryFiles+  forM_ migrationFiles (addDependentFile . (migrationsDir </>))+  when (null migrationFiles) $+    reportWarning $ "No migrations (files ending in .sql) in ‘" ++ dir ++ "’ at root of project.\n      (Looking in ‘" ++ migrationsDir ++ "’.)\n"++  migrations <- runIO $ forM migrationFiles $ \path -> (path,) <$> T.readFile (migrationsDir </> path)+  [| runMigrations' migrations |]++{-|+  The low-level API that underlies 'runMigrations'. It is unlikely that you will+  need to use this.++  Unlike 'runMigrations', this function does not use Template Haskell.+  Migrations are passed as a list of tuples, where the first element is the name+  of the migration (which must be unique) and the second element is the SQL to+  be run. The migrations will be run in the order they are provided, from left+  to right.+-}+runMigrations' :: (MonadLogger m, MonadSqlPersist m) => [(FilePath, Text)] -> m ()+runMigrations' allMigrations = do+  messages <- runMigrationSilent migrateSchema+  forM_ messages (logDebugNS "SQL")+  transactionSave+  existingMigrations <- map (schemaMigrationName . entityVal) <$> selectList [] []+  let newMigrations = filter (\(path, _) -> path `notElem` existingMigrations) allMigrations+  forM_ newMigrations $ \(path, sql) -> do+    logInfoNS "migrate" $ "migrating ‘" <> toText path <> "’"+    rawExecute sql []+    insert_ (SchemaMigration path)+    transactionSave
+ package.yaml view
@@ -0,0 +1,71 @@+name: genesis+version: 0.0.1.0+category: Other+synopsis: Opinionated bootstrapping for Haskell web services.+description: Opinionated bootstrapping for Haskell web services.+maintainer: Alexis King <lexi.lambda@gmail.com>+copyright: 2017 CJ Affiliate by Conversant+license: ISC+github: cjdev/genesis++extra-source-files:+- CHANGELOG.md+- package.yaml+- stack.yaml++ghc-options: -Wall+default-extensions:+- ConstraintKinds+- DeriveGeneric+- DuplicateRecordFields+- FlexibleContexts+- FlexibleInstances+- GADTs+- GeneralizedNewtypeDeriving+- KindSignatures+- LambdaCase+- MultiParamTypeClasses+- NamedFieldPuns+- OverloadedStrings+- RankNTypes+- ScopedTypeVariables+- TypeApplications+- TypeOperators++library:+  dependencies:+  - base                   >= 4.9.0.0 && < 5+  - directory              >= 1.2.7.0+  - envparse               >= 0.3.0 && < 1+  - file-embed             >= 0.0.10 && < 1+  - filepath+  - monad-control          >= 1.0.0.0 && < 2+  - monad-logger           >= 0.3.10+  - monad-persist          >= 0.0.1.0 && < 1+  - persistent             >= 2.5 && < 3+  - persistent-postgresql  >= 2.5 && < 3+  - persistent-template    >= 2.5 && < 3+  - resource-pool+  - template-haskell       >= 2.11.0.0 && < 2.12+  - text+  - text-conversions       >= 0.2.0 && < 1+  source-dirs: library++tests:+  genesis-test-suite:+    dependencies:+    - base+    - genesis+    - hspec+    - monad-control+    - monad-logger+    - monad-persist+    - persistent-sqlite+    - persistent-template+    - text+    ghc-options:+    - -rtsopts+    - -threaded+    - -with-rtsopts=-N+    main: Main.hs+    source-dirs: test-suite
+ stack.yaml view
@@ -0,0 +1,9 @@+resolver: lts-8.11++packages:+- '.'+extra-deps: [monad-persist-0.0.1.0]++flags: {}++extra-package-dbs: []
+ test-suite/Genesis/MigrateSpec.hs view
@@ -0,0 +1,51 @@+{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module Genesis.MigrateSpec (spec) where++import qualified Test.Hspec as Hspec++import Control.Monad.Logger (NoLoggingT, runNoLoggingT)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Persist (Entity(..), SqlPersistT, insert, runSqlPersistT, selectList)+import Control.Monad.Trans.Control (MonadBaseControl)+import Data.Text (Text)+import Database.Persist.Sqlite (withSqliteConn)+import Database.Persist.TH (mkPersist, persistLowerCase, sqlSettings)+import Test.Hspec hiding (shouldBe)++import Genesis.Persist (runMigrations)++mkPersist sqlSettings [persistLowerCase|+Blog+  name Text+  deriving Eq Show++Post+  title Text+  body Text+  blogId BlogId+  deriving Eq Show+|]++runSqlite :: (MonadIO m, MonadBaseControl IO m) => SqlPersistT (NoLoggingT m) a -> m a+runSqlite x = runNoLoggingT $ withSqliteConn ":memory:" (runSqlPersistT ($(runMigrations "test-suite/migrations") >> x))++shouldBe :: (Eq a, Show a, MonadIO m) => a -> a -> m ()+shouldBe x y = liftIO $ Hspec.shouldBe x y++spec :: Spec+spec = describe "runMigrations" $+  it "executes all SQL migrations in the specified directory" $ example $ runSqlite $ do+    blogId <- insert $ Blog "Alyssa’s Blog"+    let post = Post { postTitle = "First Post", postBody = "world changing post", postBlogId = blogId }+    postId <- insert post+    posts <- selectList [] []+    posts `shouldBe` [Entity postId post]
+ test-suite/Main.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}