Skip to content

Commit

Permalink
Add support for Prefer tx=rollback
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfgangwalther committed Nov 20, 2020
1 parent 944efb3 commit 85a334e
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 44 deletions.
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 :: 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 Rollback) -> Rollback
| hasPrefer (show Commit) -> Commit
| otherwise -> Commit
, 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
72 changes: 41 additions & 31 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,14 @@ app dbStructure conf apiRequest =
then Nothing
else (\x -> ("Preference-Applied", BS.pack (show x))) <$> iPreferResolution apiRequest
] ++ ctHeaders)) (unwrapGucHeader <$> ghdrs)
if contentType == CTSingularJSON && queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
if | contentType == CTSingularJSON && queryTotal /= 1 -> do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
| shouldRollback -> do
HT.condemn
return $ responseLBS status (addHeadersIfNotIncluded [("Preference-Applied", BS.pack (show Rollback))] headers) rBody
| otherwise -> do
return $ responseLBS status headers rBody

(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of
Expand All @@ -206,12 +208,14 @@ app dbStructure conf apiRequest =
then ([Just $ toHeader contentType, profileH], toS body)
else ([], mempty)
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
if contentType == CTSingularJSON && queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
if | contentType == CTSingularJSON && queryTotal /= 1 -> do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
| shouldRollback -> do
HT.condemn
return $ responseLBS status (addHeadersIfNotIncluded [("Preference-Applied", BS.pack (show Rollback))] headers) rBody
| otherwise -> do
return $ responseLBS status headers rBody

(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of
Expand All @@ -234,12 +238,14 @@ app dbStructure conf apiRequest =
-- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done
if queryTotal /= 1
then do
HT.condemn
return . errorResponseFor $ PutMatchingPkError
else
return $ responseLBS status headers rBody
if | queryTotal /= 1 -> do
HT.condemn
return . errorResponseFor $ PutMatchingPkError
| shouldRollback -> do
HT.condemn
return $ responseLBS status (addHeadersIfNotIncluded [("Preference-Applied", BS.pack (show Rollback))] headers) rBody
| otherwise -> do
return $ responseLBS status headers rBody

(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
case mutateSqlParts tSchema tName of
Expand All @@ -263,13 +269,14 @@ app dbStructure conf apiRequest =
then ([Just $ toHeader contentType, profileH], toS body)
else ([], mempty)
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
if contentType == CTSingularJSON
&& queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
if | contentType == CTSingularJSON && queryTotal /= 1 -> do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
| shouldRollback -> do
HT.condemn
return $ responseLBS status (addHeadersIfNotIncluded [("Preference-Applied", BS.pack (show Rollback))] headers) rBody
| otherwise -> do
return $ responseLBS status headers rBody

(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in
Expand Down Expand Up @@ -303,12 +310,14 @@ app dbStructure conf apiRequest =
(catMaybes [Just $ toHeader contentType, Just contentRange, profileH])
(unwrapGucHeader <$> ghdrs)
rBody = if invMethod == InvHead then mempty else toS body
if contentType == CTSingularJSON && queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
if | contentType == CTSingularJSON && queryTotal /= 1 -> do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
| (invMethod == InvPost && shouldRollback) -> do
HT.condemn
return $ responseLBS status (addHeadersIfNotIncluded [("Preference-Applied", BS.pack (show Rollback))] headers) rBody
| otherwise -> do
return $ responseLBS status headers rBody

(ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do
let host = configHost conf
Expand Down Expand Up @@ -343,6 +352,7 @@ app dbStructure conf apiRequest =
_ -> False
pgVer = pgVersion dbStructure
profileH = contentProfileH <$> iProfile apiRequest
shouldRollback = configAllowRollback conf && iPreferTransaction apiRequest == Rollback

readSqlParts s t =
let
Expand Down
6 changes: 6 additions & 0 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ data AppConfig = AppConfig {
, configJWKS :: Maybe JWKSet

, configLogLevel :: LogLevel
, configAllowRollback :: Bool
}

configPoolTimeout' :: (Fractional a) => AppConfig -> a
Expand Down Expand Up @@ -196,6 +197,10 @@ readPathShowHelp = customExecParser parserPrefs opts
|
|## logging level, the admitted values are: crit, error, warn and info.
|# log-level = "error"
|
|## apply Prefer: tx=rollback header
|## disabled by default
|# allow-rollback = false
|]

-- | Parse the config file
Expand Down Expand Up @@ -240,6 +245,7 @@ readAppConfig cfgPath = do
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
<*> pure Nothing
<*> parseLogLevel "log-level"
<*> ((Just False ==) <$> optBool "allow-rollback")

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

0 comments on commit 85a334e

Please sign in to comment.