Compare commits

..
34 Commits
Author SHA1 Message Date
Joe Nelson 8af91e262c Merge pull request #508 from begriffs/test-plain-build
Test that binary builds, not just that suite passes
2016-02-29 22:49:33 -08:00
Joe Nelson cf176c4100 Allow aeson v11, but forbid deadly v10 2016-02-29 21:03:42 -08:00
Joe Nelson c61418635e Ensure helper binaries get re-installed
Sadly causes all extra-deps to rebuild every time
2016-02-29 20:45:34 -08:00
Joe Nelson dba827d1fd List missing other-module in spec 2016-02-29 14:52:01 -08:00
Joe Nelson e315ad99b4 Name the main module "Main" as required 2016-02-29 14:50:44 -08:00
Joe Nelson 088df7e6be Test that binary build succeeds
Work around https://github.com/commercialhaskell/stack/issues/1846
2016-02-29 14:25:34 -08:00
Joe Nelson 4cd2475bf2 v0.3.1.0 2016-02-28 21:45:17 -08:00
Joe Nelson fc4c792f9e Move section in changelog 2016-02-26 12:17:24 -08:00
Joe Nelson c094e5a0fc Merge pull request #489 from diogob/apply_range_headers_to_rpc
Apply range headers to rpc
2016-02-26 12:10:33 -08:00
Diogo Biazus 9d0f3573c6 Implements query counting in proc call and adds Content-Rage to response
headers in /rpc calls.
2016-02-26 14:51:35 -05:00
Diogo Biazus 4496a95014 Updates changelog 2016-02-26 14:41:27 -05:00
Diogo Biazus 893b7a7126 Applies range headers to /rpc calls using LIMIT/OFFSET. 2016-02-26 14:41:27 -05:00
Joe Nelson 3b23c4aa5b Merge pull request #503 from begriffs/one-tx-per-client
Reduces pool resource locking (2)
2016-02-26 10:11:47 -08:00
Joe Nelson d466ea45ff Add changelog entry
Nice work guys, this took a lot of cooperation
2016-02-26 10:06:54 -08:00
Joe Nelson 7ba5363d25 Upgrade hasql to fix prepared statement problem 2016-02-26 08:18:47 -08:00
Joe Nelson f28b03f419 Allow new hasql-transaction to do rollbacks 2016-02-25 20:13:03 -08:00
Joe Nelson de772b9246 Modified the concurrent test to illustrate problem with prepared statement 2016-02-22 20:58:44 -08:00
Joe Nelson c28b26d949 Run QueryLimitedSpec with its own server flags 2016-02-22 19:21:39 -08:00
Joe Nelson c02dd4aa98 Enable real threads in test 2016-02-22 17:53:10 -08:00
Joe Nelson b0974a4e36 Reset db between each test suite 2016-02-22 17:50:00 -08:00
Joe Nelson 17acd134c7 Suppress server logging in test mode 2016-02-22 17:48:33 -08:00
Joe Nelson d4a4bbf966 Roll back on db errors 2016-02-22 16:52:50 -08:00
Joe Nelson 7b7babd1d1 Fix frozen tests
Problem found by @ruslantalpa
2016-02-22 08:41:31 -08:00
Joe Nelson 072a6ce4c7 Bump hasql to 0.19.8 2016-02-21 18:37:17 -08:00
Joe Nelson d5c1438c6e Use hasql-transaction
Also use hspec before-wrapper
2016-02-21 18:05:25 -08:00
Joe Nelson 30e5032ade Use lower optimization to speed up regular dev builds 2016-02-21 14:11:46 -08:00
Joe Nelson d7fe59f0b0 WIP: share server code between tests and program
- Share server code in Main
- Switch to hasql-pool
- Use pool in tests
- DRY up test runner
2016-02-21 12:22:18 -08:00
Joe Nelson 8a006f07a7 Show error text more clearly 2016-02-20 18:11:46 -08:00
Diogo BiazusandJoe Nelson 01ab540ffe Simplify return from withResource in Main.hs 2016-02-20 18:03:17 -08:00
Diogo BiazusandJoe Nelson de848f64fa Return the results from withResource function before applying the respond continuation. This ensures that the pool resource is freed as soon as the database operation is complete 2016-02-20 18:03:06 -08:00
Joe Nelson 52e689b830 Add concurrent test for "transaction in progress"
MonadBaseControl wizardry courtesy of @jwiegley
2016-02-20 17:45:55 -08:00
Joe Nelson 6b4b763bc4 Merge pull request #494 from begriffs/test-raw-cabal
Ensure plain cabal can determine a build plan
2016-02-15 11:46:31 -08:00
Joe Nelson 6b1c8b3e39 Ensure plain cabal can determine a build plan
For those wishing to use postgrest as a library
2016-02-14 22:17:26 -08:00
Joe Nelson f3293cfac1 Do not name import of void directly as it is used conditionally 2016-02-12 23:15:09 -08:00
22 changed files with 374 additions and 218 deletions
+8
View File
@@ -7,6 +7,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
## [0.3.1.0] - 2016-02-28
### Fixed
- Prevent query error from infecting later connection - @begriffs, @ruslantalpa, @nikita-volkov, @jwiegley
### Added
- Applies range headers to RPC calls - @diogob
## [0.3.0.4] - 2016-02-12 ## [0.3.0.4] - 2016-02-12
### Fixed ### Fixed
+1 -1
View File
@@ -10,7 +10,7 @@
}, },
"POSTGREST_VER": { "POSTGREST_VER": {
"description": "Version of PostgREST to deploy", "description": "Version of PostgREST to deploy",
"value": "0.3.0.4" "value": "0.3.1.0"
}, },
"DB_NAME": { "DB_NAME": {
"description": "Database name", "description": "Database name",
+4
View File
@@ -9,13 +9,17 @@ dependencies:
- createdb -O postgrest_test -U ubuntu postgrest_test - createdb -O postgrest_test -U ubuntu postgrest_test
override: override:
- stack setup - stack setup
- rm -fr $(stack path --dist-dir) $(stack path --local-install-root)
- stack install hlint packdeps cabal-install - stack install hlint packdeps cabal-install
- stack build
- stack build --test --no-run-tests - stack build --test --no-run-tests
test: test:
override: override:
- stack test - stack test
- git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@" - git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@"
- stack exec -- cabal update
- stack exec --no-ghc-package-path -- cabal install --only-d --dry-run
- stack exec -- packdeps *.cabal || true - stack exec -- packdeps *.cabal || true
- stack exec -- cabal check - stack exec -- cabal check
- stack haddock --no-haddock-deps - stack haddock --no-haddock-deps
+23 -6
View File
@@ -2,7 +2,7 @@ name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security for the tables and views, supporting all HTTP verbs that security
permits. permits.
version: 0.3.0.4 version: 0.3.1.0
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
@@ -23,10 +23,10 @@ Flag CI
executable postgrest executable postgrest
main-is: PostgREST/Main.hs main-is: PostgREST/Main.hs
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes, LambdaCase default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010 default-language: Haskell2010
build-depends: aeson >= 0.8 && < 0.10 build-depends: aeson (>= 0.8 && < 0.10) || (>= 0.11 && < 0.12)
, base >= 4.8 && < 5 , base >= 4.8 && < 5
, bytestring , bytestring
, case-insensitive , case-insensitive
@@ -34,15 +34,17 @@ executable postgrest
, containers , containers
, contravariant , contravariant
, errors , errors
, hasql >= 0.19.3.3 && < 0.20 , hasql >= 0.19.9 && < 0.20
, hasql-pool >= 0.4 && < 0.5
, hasql-transaction >= 0.4.3 && < 0.5
, http-types , http-types
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jwt
, mtl
, optparse-applicative >= 0.11 && < 0.13 , optparse-applicative >= 0.11 && < 0.13
, parsec , parsec
, postgrest , postgrest
, regex-tdfa , regex-tdfa
, resource-pool
, safe >= 0.3 && < 0.4 , safe >= 0.3 && < 0.4
, scientific , scientific
, string-conversions , string-conversions
@@ -86,9 +88,12 @@ library
, contravariant , contravariant
, errors , errors
, hasql , hasql
, hasql-transaction
, hasql-pool
, http-types , http-types
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jwt
, mtl
, optparse-applicative , optparse-applicative
, parsec , parsec
, regex-tdfa , regex-tdfa
@@ -123,14 +128,17 @@ library
Test-Suite spec Test-Suite spec
Type: exitcode-stdio-1.0 Type: exitcode-stdio-1.0
Default-Language: Haskell2010 Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes, LambdaCase default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
ghc-options: -threaded -rtsopts -with-rtsopts=-N
Hs-Source-Dirs: test, src Hs-Source-Dirs: test, src
Main-Is: Main.hs Main-Is: Main.hs
Other-Modules: Feature.AuthSpec Other-Modules: Feature.AuthSpec
, Feature.ConcurrentSpec
, Feature.CorsSpec , Feature.CorsSpec
, Feature.DeleteSpec , Feature.DeleteSpec
, Feature.InsertSpec , Feature.InsertSpec
, Feature.QuerySpec , Feature.QuerySpec
, Feature.QueryLimitedSpec
, Feature.RangeSpec , Feature.RangeSpec
, Feature.StructureSpec , Feature.StructureSpec
, Paths_postgrest , Paths_postgrest
@@ -148,6 +156,7 @@ Test-Suite spec
, SpecHelper , SpecHelper
, TestTypes , TestTypes
Build-Depends: aeson Build-Depends: aeson
, async
, base , base
, base64-string , base64-string
, bytestring , bytestring
@@ -157,6 +166,8 @@ Test-Suite spec
, contravariant , contravariant
, errors , errors
, hasql , hasql
, hasql-pool
, hasql-transaction
, heredoc , heredoc
, hspec == 2.2.* , hspec == 2.2.*
, hspec-wai , hspec-wai
@@ -164,6 +175,8 @@ Test-Suite spec
, http-types , http-types
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jwt
, monad-control
, mtl
, optparse-applicative , optparse-applicative
, parsec , parsec
, process , process
@@ -173,11 +186,15 @@ Test-Suite spec
, string-conversions , string-conversions
, text , text
, time , time
, transformers
, transformers-base
, unordered-containers , unordered-containers
, unix
, vector , vector
, wai , wai
, wai-cors , wai-cors
, wai-extra , wai-extra
, wai-middleware-static , wai-middleware-static
, warp
, HTTP , HTTP
, Ranged-sets , Ranged-sets
+53 -28
View File
@@ -3,7 +3,7 @@
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
--module PostgREST.App where --module PostgREST.App where
module PostgREST.App ( module PostgREST.App (
app postgrest
) where ) where
import Control.Applicative import Control.Applicative
@@ -18,6 +18,9 @@ import Data.String.Conversions (cs)
import Data.Text (Text, replace, strip) import Data.Text (Text, replace, strip)
import Data.Tree import Data.Tree
import qualified Hasql.Pool as P
import qualified Hasql.Transaction as HT
import Text.Parsec.Error import Text.Parsec.Error
import Text.ParserCombinators.Parsec (parse) import Text.ParserCombinators.Parsec (parse)
@@ -26,25 +29,26 @@ import Network.HTTP.Types.Header
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (parseSimpleQuery) import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai import Network.Wai
import Network.Wai.Middleware.RequestLogger (logStdout)
import Data.Aeson import Data.Aeson
import Data.Aeson.Types (emptyArray) import Data.Aeson.Types (emptyArray)
import Data.Monoid import Data.Monoid
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Hasql.Session as H import qualified Hasql.Transaction as H
import PostgREST.Config (AppConfig (..))
import PostgREST.Parsers
import PostgREST.DbStructure
import PostgREST.RangeQuery
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..) import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
, Action(..), Target(..) , Action(..), Target(..)
, PreferRepresentation (..) , PreferRepresentation (..)
, userApiRequest) , userApiRequest)
import PostgREST.Types
import PostgREST.Auth (tokenJWT) import PostgREST.Auth (tokenJWT)
import PostgREST.Error (errResponse) import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure
import PostgREST.Error (errResponse, pgErrResponse)
import PostgREST.Parsers
import PostgREST.RangeQuery
import PostgREST.Middleware
import PostgREST.QueryBuilder ( callProc import PostgREST.QueryBuilder ( callProc
, addJoinConditions , addJoinConditions
, sourceCTEName , sourceCTEName
@@ -55,10 +59,25 @@ import PostgREST.QueryBuilder ( callProc
, createWriteStatement , createWriteStatement
, ResultsWithCount , ResultsWithCount
) )
import PostgREST.Types
import Prelude import Prelude
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Session Response
postgrest :: AppConfig -> DbStructure -> P.Pool -> Application
postgrest conf dbStructure pool =
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
middle $ \ req respond -> do
time <- getPOSIXTime
body <- strictRequestBody req
let handleReq = runWithClaims conf time (app dbStructure conf body) req
resp <- either pgErrResponse id <$> P.use pool
(HT.run handleReq HT.ReadCommitted HT.Write)
respond resp
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Transaction Response
app dbStructure conf reqBody req = app dbStructure conf reqBody req =
let let
-- TODO: blow up for Left values (there is a middleware that checks the headers) -- TODO: blow up for Left values (there is a middleware that checks the headers)
@@ -71,13 +90,10 @@ app dbStructure conf reqBody req =
case readSqlParts of case readSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (q, cq) -> do Right (q, cq) -> do
let range = restrictRange (configMaxRows conf) $ iRange apiRequest let singular = iPreferSingular apiRequest
singular = iPreferSingular apiRequest
stm = createReadStatement q cq range singular stm = createReadStatement q cq range singular
(iPreferCount apiRequest) (contentType == TextCSV) shouldCount (contentType == TextCSV)
if range == emptyRange respondToRange $ do
then return $ errResponse status416 "HTTP Range error"
else do
row <- H.query () stm row <- H.query () stm
let (tableTotal, queryTotal, _ , body) = row let (tableTotal, queryTotal, _ , body) = row
if singular if singular
@@ -85,10 +101,7 @@ app dbStructure conf reqBody req =
then responseLBS status404 [] "" then responseLBS status404 [] ""
else responseLBS status200 [contentTypeH] (cs body) else responseLBS status200 [contentTypeH] (cs body)
else do else do
let frm = rangeOffset range let (status, contentRange) = rangeHeader queryTotal tableTotal
to = frm + toInteger queryTotal - 1
contentRange = contentRangeH frm to (toInteger <$> tableTotal)
status = rangeStatus frm to (toInteger <$> tableTotal)
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)? canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
. sortBy (comparing fst) . sortBy (comparing fst)
. map (join (***) cs) . map (join (***) cs)
@@ -165,14 +178,16 @@ app dbStructure conf reqBody req =
then do then do
let p = V.head payload let p = V.head payload
jwtSecret = configJwtSecret conf jwtSecret = configJwtSecret conf
respondToRange $ do
bodyJson <- H.query () (callProc qi p) row <- H.query () (callProc qi p range shouldCount)
returnJWT <- H.query qi doesProcReturnJWT returnJWT <- H.query qi doesProcReturnJWT
return $ responseLBS status200 [jsonH] let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row
(let body = fromMaybe emptyArray bodyJson in (status, contentRange) = rangeHeader queryTotal tableTotal
if returnJWT in
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}" return $ responseLBS status [jsonH, contentRange]
else cs $ encode body) (if returnJWT
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}"
else cs $ encode body)
else return notFound else return notFound
(ActionRead, TargetRoot, Nothing) -> do (ActionRead, TargetRoot, Nothing) -> do
@@ -196,6 +211,8 @@ app dbStructure conf reqBody req =
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
schema = cs $ configSchema conf schema = cs $ configSchema conf
apiRequest = userApiRequest schema req reqBody apiRequest = userApiRequest schema req reqBody
shouldCount = iPreferCount apiRequest
range = restrictRange (configMaxRows conf) $ iRange apiRequest
readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
selectQuery = requestToQuery schema <$> readDbRequest selectQuery = requestToQuery schema <$> readDbRequest
@@ -203,6 +220,14 @@ app dbStructure conf reqBody req =
mutateQuery = requestToQuery schema <$> mutateDbRequest mutateQuery = requestToQuery schema <$> mutateDbRequest
readSqlParts = (,) <$> selectQuery <*> countQuery readSqlParts = (,) <$> selectQuery <*> countQuery
mutateSqlParts = (,) <$> selectQuery <*> mutateQuery mutateSqlParts = (,) <$> selectQuery <*> mutateQuery
respondToRange response = if range == emptyRange
then return $ errResponse status416 "HTTP Range error"
else response
rangeHeader queryTotal tableTotal = let frm = rangeOffset range
to = frm + toInteger queryTotal - 1
contentRange = contentRangeH frm to (toInteger <$> tableTotal)
status = rangeStatus frm to (toInteger <$> tableTotal)
in (status, contentRange)
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
rangeStatus _ _ Nothing = status200 rangeStatus _ _ Nothing = status200
+2
View File
@@ -43,6 +43,7 @@ data AppConfig = AppConfig {
, configJwtSecret :: Secret , configJwtSecret :: Secret
, configPool :: Int , configPool :: Int
, configMaxRows :: Maybe Integer , configMaxRows :: Maybe Integer
, configQuiet :: Bool
} }
argParser :: Parser AppConfig argParser :: Parser AppConfig
@@ -55,6 +56,7 @@ argParser = AppConfig
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault)) strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault) <*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault)) <*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
<*> pure False
defaultCorsPolicy :: CorsResourcePolicy defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing defaultCorsPolicy = CorsResourcePolicy Nothing
+21 -10
View File
@@ -7,11 +7,13 @@ module PostgREST.Error (pgErrResponse, errResponse) where
import Data.Aeson ((.=)) import Data.Aeson ((.=))
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Hasql.Session as H import qualified Hasql.Session as H
import qualified Hasql.Pool as P
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import qualified Network.HTTP.Types.Status as HT import qualified Network.HTTP.Types.Status as HT
import Network.Wai (Response, responseLBS) import Network.Wai (Response, responseLBS)
@@ -19,10 +21,17 @@ import Network.Wai (Response, responseLBS)
errResponse :: HT.Status -> Text -> Response errResponse :: HT.Status -> Text -> Response
errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"]) errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"])
pgErrResponse :: H.Error -> Response pgErrResponse :: P.UsageError -> Response
pgErrResponse e = responseLBS (httpStatus e) pgErrResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e) [(hContentType, "application/json")] (JSON.encode e)
instance JSON.ToJSON P.UsageError where
toJSON (P.ConnectionError e) = JSON.object [
"code" .= ("" :: T.Text),
"message" .= ("Connection error" :: T.Text),
"details" .= (cs (fromMaybe "" e) :: T.Text)]
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
instance JSON.ToJSON H.Error where instance JSON.ToJSON H.Error where
toJSON (H.ResultError (H.ServerError c m d h)) = JSON.object [ toJSON (H.ResultError (H.ServerError c m d h)) = JSON.object [
"code" .= (cs c::T.Text), "code" .= (cs c::T.Text),
@@ -51,15 +60,17 @@ instance JSON.ToJSON H.Error where
"message" .= ("Database client error"::String), "message" .= ("Database client error"::String),
"details" .= (fmap cs d::Maybe T.Text)] "details" .= (fmap cs d::Maybe T.Text)]
httpStatus :: H.Error -> HT.Status httpStatus :: P.UsageError -> HT.Status
httpStatus (H.ResultError (H.ServerError c _ _ _)) = httpStatus (P.ConnectionError _) =
HT.status500
httpStatus (P.SessionError (H.ResultError (H.ServerError c _ _ _))) =
case cs c of case cs c of
'0':'8':_ -> HT.status503 -- pg connection err '0':'8':_ -> HT.status503 -- pg connection err
'0':'9':_ -> HT.status500 -- triggered action exception '0':'9':_ -> HT.status500 -- triggered action exception
'0':'L':_ -> HT.status403 -- invalid grantor '0':'L':_ -> HT.status403 -- invalid grantor
'0':'P':_ -> HT.status403 -- invalid role specification '0':'P':_ -> HT.status403 -- invalid role specification
"23503" -> HT.status409 -- foreign_key_violation "23503" -> HT.status409 -- foreign_key_violation
"23505" -> HT.status409 -- unique_violation "23505" -> HT.status409 -- unique_violation
'2':'5':_ -> HT.status500 -- invalid tx state '2':'5':_ -> HT.status500 -- invalid tx state
'2':'8':_ -> HT.status403 -- invalid auth specification '2':'8':_ -> HT.status403 -- invalid auth specification
'2':'D':_ -> HT.status500 -- invalid tx termination '2':'D':_ -> HT.status500 -- invalid tx termination
@@ -76,8 +87,8 @@ httpStatus (H.ResultError (H.ServerError c _ _ _)) =
'H':'V':_ -> HT.status500 -- foreign data wrapper error 'H':'V':_ -> HT.status500 -- foreign data wrapper error
'P':'0':_ -> HT.status500 -- PL/pgSQL Error 'P':'0':_ -> HT.status500 -- PL/pgSQL Error
'X':'X':_ -> HT.status500 -- internal Error 'X':'X':_ -> HT.status500 -- internal Error
"42P01" -> HT.status404 -- undefined table "42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege "42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400 _ -> HT.status400
httpStatus (H.ResultError _) = HT.status500 httpStatus (P.SessionError (H.ResultError _)) = HT.status500
httpStatus (H.ClientError _) = HT.status503 httpStatus (P.SessionError (H.ClientError _)) = HT.status503
+17 -44
View File
@@ -9,29 +9,20 @@ import PostgREST.Config (AppConfig (..),
prettyVersion, prettyVersion,
readOptions) readOptions)
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.Error (errResponse, pgErrResponse)
import PostgREST.Middleware
import PostgREST.QueryBuilder (inTransaction, Isolation(..))
import Control.Monad (unless, void) import Control.Monad
import Data.Monoid ((<>)) import Data.Monoid ((<>))
import Data.Pool
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Hasql.Query as H import qualified Hasql.Query as H
import qualified Hasql.Connection as H
import qualified Hasql.Session as H import qualified Hasql.Session as H
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE import qualified Hasql.Encoders as HE
import qualified Network.HTTP.Types.Status as HT import qualified Hasql.Pool as P
import Network.Wai
import Network.Wai.Handler.Warp import Network.Wai.Handler.Warp
import Network.Wai.Middleware.RequestLogger (logStdout)
import System.IO (BufferMode (..), import System.IO (BufferMode (..),
hSetBuffering, stderr, hSetBuffering, stderr,
stdin, stdout) stdin, stdout)
import Web.JWT (secret) import Web.JWT (secret)
#ifndef mingw32_HOST_OS #ifndef mingw32_HOST_OS
import System.Posix.Signals import System.Posix.Signals
import Control.Concurrent (myThreadId) import Control.Concurrent (myThreadId)
@@ -55,50 +46,32 @@ main = do
conf <- readOptions conf <- readOptions
let port = configPort conf let port = configPort conf
pgSettings = cs (configDatabase conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
unless (secret "secret" /= configJwtSecret conf) $ unless (secret "secret" /= configJwtSecret conf) $
putStrLn "WARNING, running in insecure mode, JWT secret is the default value" putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
Prelude.putStrLn $ "Listening on port " ++ Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String) (show $ configPort conf :: String)
let pgSettings = cs (configDatabase conf) pool <- P.acquire (configPool conf, 10, pgSettings)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle = logStdout . defaultMiddle
pool <- createPool (H.acquire pgSettings)
(either (const $ return ()) H.release) 1 1 (configPool conf)
dbStructure <- withResource pool $ \case
Left err -> error $ show err
Right c -> do
supported <- H.run isServerVersionSupported c
case supported of
Left e -> error $ show e
Right good -> unless good $
error (
"Cannot run in this PostgreSQL version, PostgREST needs at least "
<> show minimumPgVersion)
dbOrError <- H.run (getDbStructure (cs $ configSchema conf)) c
either (error . show) return dbOrError
#ifndef mingw32_HOST_OS #ifndef mingw32_HOST_OS
tid <- myThreadId tid <- myThreadId
void $ installHandler keyboardSignal (Catch $ do void $ installHandler keyboardSignal (Catch $ do
destroyAllResources pool P.release pool
throwTo tid UserInterrupt throwTo tid UserInterrupt
) Nothing ) Nothing
#endif #endif
runSettings appSettings $ middle $ \ req respond -> do result <- P.use pool $ do
time <- getPOSIXTime supported <- isServerVersionSupported
body <- strictRequestBody req unless supported $ error (
let handleReq = H.run $ inTransaction ReadCommitted "Cannot run in this PostgreSQL version, PostgREST needs at least "
(runWithClaims conf time (app dbStructure conf body) req) <> show minimumPgVersion)
withResource pool $ \case getDbStructure (cs $ configSchema conf)
Left err -> respond $ errResponse HT.status500 (cs . show $ err)
Right c -> do let dbStructure = either (error.show) id result
resOrError <- handleReq c runSettings appSettings $ postgrest conf dbStructure pool
either (respond . pgErrResponse) respond resOrError
+3 -3
View File
@@ -7,7 +7,7 @@ import Data.Maybe (fromMaybe)
import Data.Text import Data.Text
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Time.Clock (NominalDiffTime) import Data.Time.Clock (NominalDiffTime)
import qualified Hasql.Session as H import qualified Hasql.Transaction as H
import Network.HTTP.Types.Header (hAccept, hAuthorization) import Network.HTTP.Types.Header (hAccept, hAuthorization)
import Network.HTTP.Types.Status (status415, status400) import Network.HTTP.Types.Status (status415, status400)
@@ -27,8 +27,8 @@ import Prelude hiding(concat)
import qualified Data.Map.Lazy as M import qualified Data.Map.Lazy as M
runWithClaims :: AppConfig -> NominalDiffTime -> runWithClaims :: AppConfig -> NominalDiffTime ->
(Request -> H.Session Response) -> (Request -> H.Transaction Response) ->
Request -> H.Session Response Request -> H.Transaction Response
runWithClaims conf time app req = do runWithClaims conf time app req = do
H.sql setAnon H.sql setAnon
case split (== ' ') (cs auth) of case split (== ' ') (cs auth) of
+15 -27
View File
@@ -18,7 +18,6 @@ module PostgREST.QueryBuilder (
, callProc , callProc
, createReadStatement , createReadStatement
, createWriteStatement , createWriteStatement
, inTransaction
, operators , operators
, pgFmtIdent , pgFmtIdent
, pgFmtLit , pgFmtLit
@@ -27,11 +26,9 @@ module PostgREST.QueryBuilder (
, sourceCTEName , sourceCTEName
, unquoted , unquoted
, ResultsWithCount , ResultsWithCount
, Isolation(..)
) where ) where
import qualified Hasql.Query as H import qualified Hasql.Query as H
import qualified Hasql.Session as H
import qualified Hasql.Encoders as HE import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
@@ -206,18 +203,26 @@ addJoinConditions schema (Node (query, (n, r)) forest) =
updatedForest = mapM (addJoinConditions schema) forest updatedForest = mapM (addJoinConditions schema) forest
addCond query' con = query'{flt_=con ++ flt_ query'} addCond query' con = query'{flt_=con ++ flt_ query'}
callProc :: QualifiedIdentifier -> JSON.Object -> H.Query () (Maybe JSON.Value) type ProcResults = (Maybe Int64, Int64, JSON.Value)
callProc qi params = callProc :: QualifiedIdentifier -> JSON.Object -> NonnegRange -> Bool -> H.Query () (Maybe ProcResults)
H.statement sql HE.unit decodeObj True callProc qi params range countTotal =
H.statement sql HE.unit decodeProc True
where where
sql = [qc| SELECT array_to_json( sql = [qc| SELECT
{countQuery} as countTotal,
{countResult} as countResult,
array_to_json(
coalesce(array_agg(row_to_json(t)), '\{}') coalesce(array_agg(row_to_json(t)), '\{}')
)::character varying )::character varying
from ({_callSql}) t |] from (select * {_callSql} {limitF range}) t |]
_args = intercalate "," $ map _assignment (HM.toList params) _args = intercalate "," $ map _assignment (HM.toList params)
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v _assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
_callSql = [qc| select * from {fromQi qi}({_args}) |] :: BS.ByteString _callSql = [qc| from {fromQi qi}({_args}) |] :: BS.ByteString
decodeObj = HD.maybeRow (HD.value HD.json) countQuery = if countTotal then [qc| (select pg_catalog.count(1) {_callSql} c) |] else "null::bigint" :: BS.ByteString
countResult = "pg_catalog.count(t)" :: BS.ByteString
decodeProc = HD.maybeRow procRow
procRow = (,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value HD.json
operators :: [(Text, SqlFragment)] operators :: [(Text, SqlFragment)]
operators = [ operators = [
@@ -504,20 +509,3 @@ pgFmtAsJsonPath (Just xx) = " AS " <> last xx
trimNullChars :: Text -> Text trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0') trimNullChars = T.takeWhile (/= '\x0')
data Isolation = ReadCommitted | RepeatableRead | Serializable
{- |
Wrap a session in a transaction of desired isolation level
-}
inTransaction :: Isolation -> H.Session a -> H.Session a
inTransaction lvl f = do
H.sql $ "begin " <> isolate <> ";"
r <- f
H.sql "commit;"
return r
where
isolate = case lvl of
ReadCommitted -> "ISOLATION LEVEL READ COMMITTED"
RepeatableRead -> "ISOLATION LEVEL REPEATABLE READ"
Serializable -> "ISOLATION LEVEL SERIALIZABLE"
+8 -3
View File
@@ -1,10 +1,15 @@
resolver: lts-5.0 resolver: lts-5.5
extra-deps: extra-deps:
- hasql-0.19.3.3
- Ranged-sets-0.3.0 - Ranged-sets-0.3.0
- bytestring-tree-builder-0.2.5
- hasql-0.19.9
- hasql-pool-0.4
- hasql-transaction-0.4.3
- packdeps-0.4.2.1 - packdeps-0.4.2.1
- postgresql-error-codes-1
- postgresql-binary-0.8.1
ghc-options: ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities postgrest: -O1 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities
packages: packages:
- '.' - '.'
+3 -5
View File
@@ -5,15 +5,13 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Network.HTTP.Types import Network.HTTP.Types
import qualified Hasql.Connection as H
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..)) import Network.Wai (Application)
-- }}} -- }}}
spec :: DbStructure -> H.Connection -> Spec spec :: SpecWith Application
spec struct c = around (withApp cfgDefault struct c) spec = describe "authorization" $ do
$ describe "authorization" $ do
it "hides tables that anonymous does not own" $ it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404 get "/authors_only" `shouldRespondWith` 404
+51
View File
@@ -0,0 +1,51 @@
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Feature.ConcurrentSpec where
import Control.Monad (void)
import Control.Monad.Base
import Control.Monad.Trans.Control
import Control.Concurrent.Async (mapConcurrently)
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai.Internal
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (Session)
import Network.Wai (Application)
spec :: SpecWith Application
spec =
describe "Queryiny in parallel" $
it "should not raise 'transaction in progress' error" $
raceTest 10 $
get "/fakefake"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json|
{ "hint": null,
"details":null,
"code":"42P01",
"message":"relation \"test.fakefake\" does not exist"
} |]
, matchStatus = 404
, matchHeaders = []
}
raceTest :: Int -> WaiExpectation -> WaiExpectation
raceTest times = liftBaseDiscard go
where
go test = void $ mapConcurrently (const test) [1..times]
instance MonadBaseControl IO WaiSession where
type StM WaiSession a = StM Session a
liftBaseWith f = WaiSession $
liftBaseWith $ \runInBase ->
f $ \k -> runInBase (unWaiSession k)
restoreM = WaiSession . restoreM
{-# INLINE liftBaseWith #-}
{-# INLINE restoreM #-}
instance MonadBase IO WaiSession where
liftBase = liftIO
+4 -4
View File
@@ -5,16 +5,16 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Network.Wai.Test (SResponse(simpleHeaders, simpleBody)) import Network.Wai.Test (SResponse(simpleHeaders, simpleBody))
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Hasql.Connection as H
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai (Application)
-- }}} -- }}}
spec :: DbStructure -> H.Connection -> Spec spec :: SpecWith Application
spec struct c = around (withApp cfgDefault struct c) $ describe "CORS" $ do spec =
describe "CORS" $ do
let preflightHeaders = [ let preflightHeaders = [
("Accept", "*/*"), ("Accept", "*/*"),
("Origin", "http://example.com"), ("Origin", "http://example.com"),
+3 -7
View File
@@ -4,15 +4,11 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Text.Heredoc import Text.Heredoc
import SpecHelper
import PostgREST.Types (DbStructure(..))
import qualified Hasql.Connection as H
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai (Application)
spec :: DbStructure -> H.Connection -> Spec spec :: SpecWith Application
spec struct c = beforeAll resetDb spec =
. around (withApp cfgDefault struct c) $
describe "Deleting" $ do describe "Deleting" $ do
context "existing record" $ do context "existing record" $ do
it "succeeds with 204 and deletion count" $ it "succeeds with 204 and deletion count" $
+3 -4
View File
@@ -6,7 +6,6 @@ import Test.Hspec.Wai.JSON
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus)) import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..))
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
@@ -14,12 +13,12 @@ import Text.Heredoc
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import Network.HTTP.Types import Network.HTTP.Types
import Control.Monad (replicateM_) import Control.Monad (replicateM_)
import qualified Hasql.Connection as H
import TestTypes(IncPK(..), CompoundPK(..)) import TestTypes(IncPK(..), CompoundPK(..))
import Network.Wai (Application)
spec :: DbStructure -> H.Connection -> Spec spec :: SpecWith Application
spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do spec = do
describe "Posting new record" $ do describe "Posting new record" $ do
context "disparate json types" $ do context "disparate json types" $ do
it "accepts disparate json types" $ do it "accepts disparate json types" $ do
+3 -6
View File
@@ -5,15 +5,12 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus)) import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
import qualified Hasql.Connection as H
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..)) import Network.Wai (Application)
spec :: DbStructure -> H.Connection -> Spec spec :: SpecWith Application
spec struct c = spec =
beforeAll resetDb
. around (withApp (cfgLimitRows 3) struct c) $
describe "Requesting many items with server limits enabled" $ do describe "Requesting many items with server limits enabled" $ do
it "restricts results" $ it "restricts results" $
get "/items" get "/items"
+14 -5
View File
@@ -5,14 +5,13 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders)) import Network.Wai.Test (SResponse(simpleHeaders))
import qualified Hasql.Connection as H
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..))
import Text.Heredoc import Text.Heredoc
import Network.Wai (Application)
spec :: DbStructure -> H.Connection -> Spec spec :: SpecWith Application
spec struct c = around (withApp cfgDefault struct c) $ do spec = do
describe "Querying a table with a column called count" $ describe "Querying a table with a column called count" $
it "should not confuse count column with pg_catalog.count aggregate" $ it "should not confuse count column with pg_catalog.count aggregate" $
@@ -371,7 +370,17 @@ spec struct c = around (withApp cfgDefault struct c) $ do
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |] [json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
describe "remote procedure call" $ do describe "remote procedure call" $ do
context "a proc that returns a set" $ context "a proc that returns a set" $ do
it "returns paginated results" $
request methodPost "/rpc/getitemrange"
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":3}] |]
, matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-0/2"]
}
it "returns proper json" $ it "returns proper json" $
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith` post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |] [json| [ {"id": 3}, {"id":4} ] |]
+100 -6
View File
@@ -5,16 +5,110 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus)) import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
import qualified Hasql.Connection as H
import qualified Data.ByteString.Lazy as BL
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..)) import Network.Wai (Application)
spec :: DbStructure -> H.Connection -> Spec defaultRange :: BL.ByteString
spec struct c = beforeAll resetDb defaultRange = [json| { "min": 0, "max": 15 } |]
. around (withApp cfgDefault struct c) $
emptyRange :: BL.ByteString
emptyRange = [json| { "min": 2, "max": 2 } |]
spec :: SpecWith Application
spec = do
describe "POST /rpc/getitemrange" $ do
context "without range headers" $ do
context "with response under server size limit" $
it "returns whole range with status 200" $
post "/rpc/getitemrange" defaultRange `shouldRespondWith` 200
context "when I don't want the count" $ do
it "returns range Content-Range with */* for empty range" $
request methodPost "/rpc/getitemrange"
[("Prefer", "count=none")] emptyRange
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns range Content-Range with range/*" $
request methodPost "/rpc/getitemrange"
[("Prefer", "count=none")] defaultRange
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
context "with range headers" $ do
context "of acceptable range" $ do
it "succeeds with partial content" $ do
r <- request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/15"
simpleStatus r `shouldBe` partialContent206
it "understands open-ended ranges" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFrom 0) defaultRange
`shouldRespondWith` 200
it "returns an empty body when there are no results" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 1) emptyRange
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/0"]
}
it "allows one-item requests" $ do
r <- request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-0/15"
simpleStatus r `shouldBe` partialContent206
it "handles ranges beyond collection length via truncation" $ do
r <- request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "10-14/15"
simpleStatus r `shouldBe` partialContent206
context "of invalid range" $ do
it "fails with 416 for offside range" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 1 0) emptyRange
`shouldRespondWith` 416
it "refuses a range with nonzero start when there are no items" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 1 2) emptyRange
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
}
it "refuses a range requesting start past last item" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 100 199) defaultRange
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/15"]
}
describe "GET /items" $ do describe "GET /items" $ do
context "without range headers" $ do context "without range headers" $ do
context "with response under server size limit" $ context "with response under server size limit" $
it "returns whole range with status 200" $ it "returns whole range with status 200" $
+4 -4
View File
@@ -3,15 +3,15 @@ module Feature.StructureSpec where
import Test.Hspec hiding (pendingWith) import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import qualified Hasql.Connection as H
import SpecHelper import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai (Application)
spec :: SpecWith Application
spec = do
spec :: DbStructure -> H.Connection -> Spec
spec struct c = around (withApp cfgDefault struct c) $ do
describe "GET /" $ do describe "GET /" $ do
it "lists views in schema" $ it "lists views in schema" $
request methodGet "/" [] "" request methodGet "/" [] ""
+26 -19
View File
@@ -3,13 +3,14 @@ module Main where
import Test.Hspec import Test.Hspec
import SpecHelper import SpecHelper
import qualified Hasql.Session as H import qualified Hasql.Pool as P
import qualified Hasql.Connection as H
import PostgREST.DbStructure (getDbStructure) import PostgREST.DbStructure (getDbStructure)
import PostgREST.App (postgrest)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import qualified Feature.AuthSpec import qualified Feature.AuthSpec
import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec import qualified Feature.DeleteSpec
import qualified Feature.InsertSpec import qualified Feature.InsertSpec
@@ -22,22 +23,28 @@ main :: IO ()
main = do main = do
setupDb setupDb
H.acquire (cs dbString) >>= \case pool <- P.acquire (3, 10, cs testDbConn)
Left err -> error $ show err
Right c -> do result <- P.use pool $ getDbStructure "test"
dbOrErr <- H.run (getDbStructure "test") c let dbStructure = either (error.show) id result
-- Not using hspec-discover because we want to precompute withApp = return $ postgrest testCfg dbStructure pool
-- the db structure and pass it to specs for speed ltdApp = return $ postgrest testLtdRowsCfg dbStructure pool
either (error.show) (hspec . specs c) dbOrErr
H.release c hspec $ do
mapM_ (beforeAll_ resetDb . before withApp) specs
-- this test runs with a different server flag
beforeAll_ resetDb . before ltdApp $
describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec
where where
specs conn dbStructure = do specs = map (uncurry describe) [
describe "Feature.AuthSpec" $ Feature.AuthSpec.spec dbStructure conn ("Feature.AuthSpec" , Feature.AuthSpec.spec)
describe "Feature.CorsSpec" $ Feature.CorsSpec.spec dbStructure conn , ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
describe "Feature.DeleteSpec" $ Feature.DeleteSpec.spec dbStructure conn , ("Feature.CorsSpec" , Feature.CorsSpec.spec)
describe "Feature.InsertSpec" $ Feature.InsertSpec.spec dbStructure conn , ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
describe "Feature.QueryLimitedSpec" $ Feature.QueryLimitedSpec.spec dbStructure conn , ("Feature.InsertSpec" , Feature.InsertSpec.spec)
describe "Feature.QuerySpec" $ Feature.QuerySpec.spec dbStructure conn , ("Feature.QuerySpec" , Feature.QuerySpec.spec)
describe "Feature.RangeSpec" $ Feature.RangeSpec.spec dbStructure conn , ("Feature.RangeSpec" , Feature.RangeSpec.spec)
describe "Feature.StructureSpec" $ Feature.StructureSpec.spec dbStructure conn , ("Feature.StructureSpec" , Feature.StructureSpec.spec)
]
+8 -36
View File
@@ -1,10 +1,6 @@
module SpecHelper where module SpecHelper where
import Network.Wai
import Test.Hspec
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Control.Monad (void) import Control.Monad (void)
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange, import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
@@ -16,42 +12,18 @@ import qualified Data.ByteString.Char8 as BS
import System.Process (readProcess) import System.Process (readProcess)
import Web.JWT (secret) import Web.JWT (secret)
import qualified Hasql.Connection as H
import qualified Hasql.Session as H
import PostgREST.App (app)
import PostgREST.Config (AppConfig(..)) import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware
import PostgREST.Error(pgErrResponse)
import PostgREST.Types
import PostgREST.QueryBuilder (inTransaction, Isolation(..))
dbString :: String testDbConn :: String
dbString = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test" testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
cfg :: String -> Maybe Integer -> AppConfig testCfg :: AppConfig
cfg conStr = AppConfig conStr "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 testCfg =
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 Nothing True
cfgDefault :: AppConfig testLtdRowsCfg :: AppConfig
cfgDefault = cfg dbString Nothing testLtdRowsCfg =
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 (Just 3) True
cfgLimitRows :: Integer -> AppConfig
cfgLimitRows = cfg dbString . Just
withApp :: AppConfig -> DbStructure -> H.Connection
-> ActionWith Application -> IO ()
withApp config dbStructure c perform =
perform $ defaultMiddle $ \req resp -> do
time <- getPOSIXTime
body <- strictRequestBody req
let handleReq = H.run $ inTransaction ReadCommitted
(runWithClaims config time (app dbStructure config body) req)
handleReq c >>= \case
Left err -> do
void $ H.run (H.sql "rollback;") c
resp $ pgErrResponse err
Right res -> resp res
setupDb :: IO () setupDb :: IO ()
setupDb = do setupDb = do