Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Prefer tx=rollback #1659

Merged
merged 1 commit into from
Nov 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1559, No downtime when reloading the schema cache with SIGUSR1 - @steve-chavez
- #504, Add `log-level` config option. The admitted levels are: crit, error, warn and info - @steve-chavez
- #1607, Enable embedding through multiple views recursively - @wolfgangwalther
- #1598, Allow rollback of the transaction with Prefer tx=rollback - @wolfgangwalther

### Fixed

Expand Down
7 changes: 4 additions & 3 deletions postgrest.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,10 @@ test-suite spec
Feature.DeleteSpec
Feature.EmbedDisambiguationSpec
Feature.ExtraSearchPathSpec
Feature.HtmlRawOutputSpec
Feature.InsertSpec
Feature.JsonOperatorSpec
Feature.MultipleSchemaSpec
Feature.NoJwtSpec
Feature.NonexistentSchemaSpec
Feature.OpenApiSpec
Expand All @@ -164,16 +166,15 @@ test-suite spec
Feature.QueryLimitedSpec
Feature.QuerySpec
Feature.RangeSpec
Feature.RawOutputTypesSpec
Feature.RollbackSpec
Feature.RootSpec
Feature.RpcPreRequestGucsSpec
Feature.RpcSpec
Feature.SingularSpec
Feature.UnicodeSpec
Feature.UpdateSpec
Feature.UpsertSpec
Feature.RawOutputTypesSpec
Feature.HtmlRawOutputSpec
Feature.MultipleSchemaSpec
SpecHelper
TestTypes
hs-source-dirs: test
Expand Down
24 changes: 14 additions & 10 deletions src/PostgREST/ApiRequest.hs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ data ApiRequest = ApiRequest {
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
, iPreferResolution :: Maybe PreferResolution -- ^ Whether the client wants to UPSERT or ignore records on PK conflict
, iPreferTransaction :: Maybe PreferTransaction -- ^ Whether the clients wants to commit or rollback the transaction
, iFilters :: [(Text, Text)] -- ^ Filters on the result ("id", "eq.10")
, iLogic :: [(Text, Text)] -- ^ &and and &or parameters used for complex boolean logic
, iSelect :: Maybe Text -- ^ &select parameter used to shape the response
Expand Down Expand Up @@ -146,16 +147,19 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
, iPayload = relevantPayload
, iPreferRepresentation = representation
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
| hasPrefer (show MultipleObjects) -> Just MultipleObjects
| otherwise -> Nothing
, iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
| hasPrefer (show PlannedCount) -> Just PlannedCount
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
| otherwise -> Nothing
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
| otherwise -> Nothing
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
| hasPrefer (show MultipleObjects) -> Just MultipleObjects
| otherwise -> Nothing
, iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
| hasPrefer (show PlannedCount) -> Just PlannedCount
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
| otherwise -> Nothing
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
| otherwise -> Nothing
, iPreferTransaction = if | hasPrefer (show Commit) -> Just Commit
| hasPrefer (show Rollback) -> Just Rollback
| otherwise -> Nothing
, iFilters = filters
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
, iSelect = toS <$> join (lookup "select" qParams)
Expand Down
10 changes: 9 additions & 1 deletion src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ postgrest logLev refConf refDbStructure pool getTime connWorker =
Right claims -> do
let
authed = containsRole claims
handleReq = runPgLocals conf claims (app dbStructure conf) apiRequest
shouldCommit = configTxAllowOverride conf && iPreferTransaction apiRequest == Just Commit
shouldRollback = configTxAllowOverride conf && iPreferTransaction apiRequest == Just Rollback
preferenceApplied
| shouldCommit = addHeadersIfNotIncluded [(hPreferenceApplied, BS.pack (show Commit))]
| shouldRollback = addHeadersIfNotIncluded [(hPreferenceApplied, BS.pack (show Rollback))]
| otherwise = identity
handleReq = do
when (shouldRollback || (configTxRollbackAll conf && not shouldCommit)) HT.condemn
mapResponseHeaders preferenceApplied <$> runPgLocals conf claims (app dbStructure conf) apiRequest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL about mapeResponseHeaders. Maybe that can be used for the Prefer: return as well. That would solve #740.

dbResp <- P.use pool $ HT.transaction HT.ReadCommitted (txMode apiRequest) handleReq
return $ either (errorResponseFor . PgError authed) identity dbResp
-- Launch the connWorker when the connection is down. The postgrest function can respond successfully(with a stale schema cache) before the connWorker is done.
Expand Down
18 changes: 16 additions & 2 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ data AppConfig = AppConfig {
, configJWKS :: Maybe JWKSet

, configLogLevel :: LogLevel

, configTxRollbackAll :: Bool
, configTxAllowOverride :: Bool
}

configPoolTimeout' :: (Fractional a) => AppConfig -> a
Expand Down Expand Up @@ -196,6 +199,15 @@ readPathShowHelp = customExecParser parserPrefs opts
|
|## logging level, the admitted values are: crit, error, warn and info.
|# log-level = "error"
|
|## rollback all transactions by default, use for test environments
|## disabled by default
|# tx-rollback-all = false
|
|## allow overriding the tx-rollback-all setting for a request by
|## setting the Prefer: tx=[commit|rollback] header
|## disabled by default
|# tx-allow-override = false
|]

-- | Parse the config file
Expand Down Expand Up @@ -225,9 +237,9 @@ readAppConfig cfgPath = do
<*> (fmap unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> (fromMaybe "pgrst" <$> optString "db-channel")
<*> ((Just True ==) <$> optBool "db-channel-enabled")
<*> (fromMaybe False <$> optBool "db-channel-enabled")
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
<*> ((Just True ==) <$> optBool "secret-is-base64")
<*> (fromMaybe False <$> optBool "secret-is-base64")
<*> parseJwtAudience "jwt-aud"
<*> (fromMaybe 10 <$> optInt "db-pool")
<*> (fromMaybe 10 <$> optInt "db-pool-timeout")
Expand All @@ -240,6 +252,8 @@ readAppConfig cfgPath = do
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
<*> pure Nothing
<*> parseLogLevel "log-level"
<*> (fromMaybe False <$> optBool "tx-rollback-all")
<*> (fromMaybe False <$> optBool "tx-allow-override")

parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode)
parseSocketFileMode k =
Expand Down
9 changes: 9 additions & 0 deletions src/PostgREST/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ instance Show PreferCount where
show PlannedCount = "count=planned"
show EstimatedCount = "count=estimated"

data PreferTransaction
= Commit -- Commit transaction - the default.
| Rollback -- Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
deriving Eq

instance Show PreferTransaction where
show Commit = "tx=commit"
show Rollback = "tx=rollback"

data DbStructure = DbStructure {
dbTables :: [Table]
, dbColumns :: [Column]
Expand Down
218 changes: 218 additions & 0 deletions test/Feature/RollbackSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
module Feature.RollbackSpec where

import Network.Wai (Application)

import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON

import Protolude hiding (get)
import SpecHelper

-- two helpers functions to make sure that each test can setup and cleanup properly

-- creates Item to work with for PATCH and DELETE
postItem =
request methodPost "/items"
[("Prefer", "resolution=ignore-duplicates")]
[json|{"id":0}|]
`shouldRespondWith`
""
{ matchStatus = 201 }

-- removes Items left over from POST, PUT, and PATCH
deleteItems =
delete "/items?id=lte.0"
`shouldRespondWith`
""
{ matchStatus = 204 }

preferDefault = [("Prefer", "return=representation")]
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
preferRollback = [("Prefer", "return=representation"), ("Prefer", "tx=rollback")]

withoutPreferenceApplied = []
withPreferenceCommitApplied = [ "Preference-Applied" <:> "tx=commit" ]
withPreferenceRollbackApplied = [ "Preference-Applied" <:> "tx=rollback" ]

shouldRespondToReads reqHeaders respHeaders = do
it "responds to GET" $ do
request methodGet "/items?id=eq.1"
reqHeaders
""
`shouldRespondWith`
[json|[{"id":1}]|]
{ matchHeaders = respHeaders }

it "responds to HEAD" $ do
request methodHead "/items?id=eq.1"
reqHeaders
""
`shouldRespondWith`
""
{ matchHeaders = respHeaders }

it "responds to GET on RPC" $ do
request methodGet "/rpc/search?id=1"
reqHeaders
""
`shouldRespondWith`
[json|[{"id":1}]|]
{ matchHeaders = respHeaders }

it "responds to POST on RPC" $ do
request methodPost "/rpc/search"
reqHeaders
[json|{"id":1}|]
`shouldRespondWith`
[json|[{"id":1}]|]
{ matchHeaders = respHeaders }

shouldPersistMutations reqHeaders respHeaders = do
it "does persist post" $ do
request methodPost "/items"
reqHeaders
[json|{"id":0}|]
`shouldRespondWith`
[json|[{"id":0}]|]
{ matchStatus = 201
, matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[{"id":0}]|]
deleteItems

it "does persist put" $ do
request methodPut "/items?id=eq.0"
reqHeaders
[json|{"id":0}|]
`shouldRespondWith`
[json|[{"id":0}]|]
{ matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[{"id":0}]|]
deleteItems

it "does persist patch" $ do
postItem
request methodPatch "/items?id=eq.0"
reqHeaders
[json|{"id":-1}|]
`shouldRespondWith`
[json|[{"id":-1}]|]
{ matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[]|]
get "items?id=eq.-1"
`shouldRespondWith`
[json|[{"id":-1}]|]
deleteItems

it "does persist delete" $ do
postItem
request methodDelete "/items?id=eq.0"
reqHeaders
""
`shouldRespondWith`
[json|[{"id":0}]|]
{ matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[]|]

shouldNotPersistMutations reqHeaders respHeaders = do
it "does not persist post" $ do
request methodPost "/items"
reqHeaders
[json|{"id":0}|]
`shouldRespondWith`
[json|[{"id":0}]|]
{ matchStatus = 201
, matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[]|]

it "does not persist put" $ do
request methodPut "/items?id=eq.0"
reqHeaders
[json|{"id":0}|]
`shouldRespondWith`
[json|[{"id":0}]|]
{ matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[]|]

it "does not persist patch" $ do
request methodPatch "/items?id=eq.1"
reqHeaders
[json|{"id":0}|]
`shouldRespondWith`
[json|[{"id":0}]|]
{ matchHeaders = respHeaders }
get "items?id=eq.0"
`shouldRespondWith`
[json|[]|]
get "items?id=eq.1"
`shouldRespondWith`
[json|[{"id":1}]|]

it "does not persist delete" $ do
request methodDelete "/items?id=eq.1"
reqHeaders
""
`shouldRespondWith`
[json|[{"id":1}]|]
{ matchHeaders = respHeaders }
get "items?id=eq.1"
`shouldRespondWith`
[json|[{"id":1}]|]

allowed :: SpecWith ((), Application)
allowed = describe "tx-allow-override = true" $ do
describe "without Prefer tx" $ do
-- TODO: Change this to default to rollback for whole test-suite
preferDefault `shouldRespondToReads` withoutPreferenceApplied
preferDefault `shouldPersistMutations` withoutPreferenceApplied

describe "Prefer tx=commit" $ do
preferCommit `shouldRespondToReads` withPreferenceCommitApplied
preferCommit `shouldPersistMutations` withPreferenceCommitApplied

describe "Prefer tx=rollback" $ do
preferRollback `shouldRespondToReads` withPreferenceRollbackApplied
preferRollback `shouldNotPersistMutations` withPreferenceRollbackApplied

disallowed :: SpecWith ((), Application)
disallowed = describe "tx-rollback-all = false, tx-allow-override = false" $ do
describe "without Prefer tx" $ do
preferDefault `shouldRespondToReads` withoutPreferenceApplied
preferDefault `shouldPersistMutations` withoutPreferenceApplied

describe "Prefer tx=commit" $ do
preferCommit `shouldRespondToReads` withoutPreferenceApplied
preferCommit `shouldPersistMutations` withoutPreferenceApplied

describe "Prefer tx=rollback" $ do
preferRollback `shouldRespondToReads` withoutPreferenceApplied
preferRollback `shouldPersistMutations` withoutPreferenceApplied


forced :: SpecWith ((), Application)
forced = describe "tx-rollback-all = true, tx-allow-override = false" $ do
describe "without Prefer tx" $ do
preferDefault `shouldRespondToReads` withoutPreferenceApplied
preferDefault `shouldNotPersistMutations` withoutPreferenceApplied

describe "Prefer tx=commit" $ do
preferCommit `shouldRespondToReads` withoutPreferenceApplied
preferCommit `shouldNotPersistMutations` withoutPreferenceApplied

describe "Prefer tx=rollback" $ do
preferRollback `shouldRespondToReads` withoutPreferenceApplied
preferRollback `shouldNotPersistMutations` withoutPreferenceApplied

Loading