feat: add max-affected preference to prefer header (#3083)

This commit is contained in:
Taimoor Zaeem
2023-12-18 18:35:23 -05:00
committed by GitHub
parent 4a1dee453b
commit 0b77098460
7 changed files with 161 additions and 42 deletions
+2
View File
@@ -5,6 +5,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
- #2887, Add Preference `max-affected` to limit affected resources - @taimoorzaeem
## [12.0.1] - 2023-12-12 ## [12.0.1] - 2023-12-12
### Fixed ### Fixed
+29 -6
View File
@@ -17,6 +17,7 @@ module PostgREST.ApiRequest.Preferences
, PreferResolution(..) , PreferResolution(..)
, PreferTransaction(..) , PreferTransaction(..)
, PreferTimezone(..) , PreferTimezone(..)
, PreferMaxAffected(..)
, fromHeaders , fromHeaders
, shouldCount , shouldCount
, prefAppliedHeader , prefAppliedHeader
@@ -42,6 +43,7 @@ import Protolude
-- >>> deriving instance Show PreferMissing -- >>> deriving instance Show PreferMissing
-- >>> deriving instance Show PreferHandling -- >>> deriving instance Show PreferHandling
-- >>> deriving instance Show PreferTimezone -- >>> deriving instance Show PreferTimezone
-- >>> deriving instance Show PreferMaxAffected
-- >>> deriving instance Show Preferences -- >>> deriving instance Show Preferences
-- | Preferences recognized by the application. -- | Preferences recognized by the application.
@@ -55,6 +57,7 @@ data Preferences
, preferMissing :: Maybe PreferMissing , preferMissing :: Maybe PreferMissing
, preferHandling :: Maybe PreferHandling , preferHandling :: Maybe PreferHandling
, preferTimezone :: Maybe PreferTimezone , preferTimezone :: Maybe PreferTimezone
, preferMaxAffected :: Maybe PreferMaxAffected
, invalidPrefs :: [ByteString] , invalidPrefs :: [ByteString]
} }
@@ -64,7 +67,7 @@ data Preferences
-- >>> let sc = S.fromList ["America/Los_Angeles"] -- >>> let sc = S.fromList ["America/Los_Angeles"]
-- --
-- One header with comma-separated values can be used to set multiple preferences: -- One header with comma-separated values can be used to set multiple preferences:
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles")] -- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100")]
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -75,12 +78,14 @@ data Preferences
-- , preferHandling = Nothing -- , preferHandling = Nothing
-- , preferTimezone = Just -- , preferTimezone = Just
-- ( PreferTimezone "America/Los_Angeles" ) -- ( PreferTimezone "America/Los_Angeles" )
-- , preferMaxAffected = Just
-- ( PreferMaxAffected 100 )
-- , invalidPrefs = [] -- , invalidPrefs = []
-- } -- }
-- --
-- Multiple headers can also be used: -- Multiple headers can also be used:
-- --
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid")] -- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid"), ("Prefer", "max-affected=5999")]
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -90,6 +95,8 @@ data Preferences
-- , preferMissing = Just ApplyNulls -- , preferMissing = Just ApplyNulls
-- , preferHandling = Just Lenient -- , preferHandling = Just Lenient
-- , preferTimezone = Nothing -- , preferTimezone = Nothing
-- , preferMaxAffected = Just
-- ( PreferMaxAffected 5999 )
-- , invalidPrefs = [ "invalid" ] -- , invalidPrefs = [ "invalid" ]
-- } -- }
-- --
@@ -121,6 +128,7 @@ data Preferences
-- , preferMissing = Just ApplyDefaults -- , preferMissing = Just ApplyDefaults
-- , preferHandling = Just Strict -- , preferHandling = Just Strict
-- , preferTimezone = Nothing -- , preferTimezone = Nothing
-- , preferMaxAffected = Nothing
-- , invalidPrefs = [ "anything" ] -- , invalidPrefs = [ "anything" ]
-- } -- }
-- --
@@ -135,7 +143,8 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls] , preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient] , preferHandling = parsePrefs [Strict, Lenient]
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing , preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
, invalidPrefs = filter checkPrefs prefs , preferMaxAffected = PreferMaxAffected <$> maxAffectedPref
, invalidPrefs = filter isUnacceptable prefs
} }
where where
mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString] mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]
@@ -151,10 +160,16 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
timezonePref = listToMaybe $ mapMaybe (BS.stripPrefix "timezone=") prefs listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList
timezonePref = listStripPrefix "timezone=" prefs
isTimezonePrefAccepted = (S.member <$> timezonePref <*> pure acceptedTzNames) == Just True isTimezonePrefAccepted = (S.member <$> timezonePref <*> pure acceptedTzNames) == Just True
checkPrefs p = p `notElem` acceptedPrefs && not isTimezonePrefAccepted maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
isUnacceptable p = p `notElem` acceptedPrefs &&
(isNothing (BS.stripPrefix "timezone=" p) || not isTimezonePrefAccepted) &&
isNothing (BS.stripPrefix "max-affected=" p)
parsePrefs :: ToHeaderValue a => [a] -> Maybe a parsePrefs :: ToHeaderValue a => [a] -> Maybe a
parsePrefs vals = parsePrefs vals =
@@ -164,7 +179,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref)) prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
prefAppliedHeader :: Preferences -> Maybe HTTP.Header prefAppliedHeader :: Preferences -> Maybe HTTP.Header
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone } = prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
if null prefsVals if null prefsVals
then Nothing then Nothing
else Just (HTTP.hPreferenceApplied, combined) else Just (HTTP.hPreferenceApplied, combined)
@@ -179,6 +194,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
, toHeaderValue <$> preferTransaction , toHeaderValue <$> preferTransaction
, toHeaderValue <$> preferHandling , toHeaderValue <$> preferHandling
, toHeaderValue <$> preferTimezone , toHeaderValue <$> preferTimezone
, if preferHandling == Just Strict then toHeaderValue <$> preferMaxAffected else Nothing
] ]
-- | -- |
@@ -278,3 +294,10 @@ newtype PreferTimezone = PreferTimezone ByteString
instance ToHeaderValue PreferTimezone where instance ToHeaderValue PreferTimezone where
toHeaderValue (PreferTimezone tz) = "timezone=" <> tz toHeaderValue (PreferTimezone tz) = "timezone=" <> tz
-- |
-- Limit Affected Resources
newtype PreferMaxAffected = PreferMaxAffected Int64
instance ToHeaderValue PreferMaxAffected where
toHeaderValue (PreferMaxAffected n) = "max-affected=" <> show n
+1
View File
@@ -96,6 +96,7 @@ data ApiRequestError
| PutMatchingPkError | PutMatchingPkError
| SingularityError Integer | SingularityError Integer
| PGRSTParseError | PGRSTParseError
| MaxAffectedViolationError Integer
deriving Show deriving Show
data QPError = QPError Text Text data QPError = QPError Text Text
+37 -28
View File
@@ -62,35 +62,36 @@ class (JSON.ToJSON a) => PgrstError a where
responseLBS (status err) (baseHeader : headers err) $ errorPayload err responseLBS (status err) (baseHeader : headers err) $ errorPayload err
instance PgrstError ApiRequestError where instance PgrstError ApiRequestError where
status AggregatesNotAllowed{} = HTTP.status400 status AggregatesNotAllowed{} = HTTP.status400
status AmbiguousRelBetween{} = HTTP.status300 status AmbiguousRelBetween{} = HTTP.status300
status AmbiguousRpc{} = HTTP.status300 status AmbiguousRpc{} = HTTP.status300
status MediaTypeError{} = HTTP.status415 status MediaTypeError{} = HTTP.status415
status InvalidBody{} = HTTP.status400 status InvalidBody{} = HTTP.status400
status InvalidFilters = HTTP.status405 status InvalidFilters = HTTP.status405
status InvalidPreferences{} = HTTP.status400 status InvalidPreferences{} = HTTP.status400
status InvalidRpcMethod{} = HTTP.status405 status InvalidRpcMethod{} = HTTP.status405
status InvalidRange{} = HTTP.status416 status InvalidRange{} = HTTP.status416
status NotFound = HTTP.status404 status NotFound = HTTP.status404
status NoRelBetween{} = HTTP.status400 status NoRelBetween{} = HTTP.status400
status NoRpc{} = HTTP.status404 status NoRpc{} = HTTP.status404
status NotEmbedded{} = HTTP.status400 status NotEmbedded{} = HTTP.status400
status PutLimitNotAllowedError = HTTP.status400 status PutLimitNotAllowedError = HTTP.status400
status QueryParamError{} = HTTP.status400 status QueryParamError{} = HTTP.status400
status RelatedOrderNotToOne{} = HTTP.status400 status RelatedOrderNotToOne{} = HTTP.status400
status SpreadNotToOne{} = HTTP.status400 status SpreadNotToOne{} = HTTP.status400
status UnacceptableFilter{} = HTTP.status400 status UnacceptableFilter{} = HTTP.status400
status UnacceptableSchema{} = HTTP.status406 status UnacceptableSchema{} = HTTP.status406
status UnsupportedMethod{} = HTTP.status405 status UnsupportedMethod{} = HTTP.status405
status LimitNoOrderError = HTTP.status400 status LimitNoOrderError = HTTP.status400
status ColumnNotFound{} = HTTP.status400 status ColumnNotFound{} = HTTP.status400
status GucHeadersError = HTTP.status500 status GucHeadersError = HTTP.status500
status GucStatusError = HTTP.status500 status GucStatusError = HTTP.status500
status OffLimitsChangesError{} = HTTP.status400 status OffLimitsChangesError{} = HTTP.status400
status PutMatchingPkError = HTTP.status400 status PutMatchingPkError = HTTP.status400
status SingularityError{} = HTTP.status406 status SingularityError{} = HTTP.status406
status PGRSTParseError = HTTP.status500 status PGRSTParseError = HTTP.status500
status MaxAffectedViolationError{} = HTTP.status400
headers SingularityError{} = [MediaType.toContentType $ MTVndSingularJSON False] headers SingularityError{} = [MediaType.toContentType $ MTVndSingularJSON False]
headers _ = mempty headers _ = mempty
@@ -199,6 +200,12 @@ instance JSON.ToJSON ApiRequestError where
toJSON AggregatesNotAllowed = toJsonPgrstError toJSON AggregatesNotAllowed = toJsonPgrstError
ApiRequestErrorCode23 "Use of aggregate functions is not allowed" Nothing Nothing ApiRequestErrorCode23 "Use of aggregate functions is not allowed" Nothing Nothing
toJSON (MaxAffectedViolationError n) = toJsonPgrstError
ApiRequestErrorCode24
"Query result exceeds max-affected preference constraint"
(Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"])
Nothing
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
SchemaCacheErrorCode00 SchemaCacheErrorCode00
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache") ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
@@ -606,6 +613,7 @@ data ErrorCode
| ApiRequestErrorCode21 | ApiRequestErrorCode21
| ApiRequestErrorCode22 | ApiRequestErrorCode22
| ApiRequestErrorCode23 | ApiRequestErrorCode23
| ApiRequestErrorCode24
-- Schema Cache errors -- Schema Cache errors
| SchemaCacheErrorCode00 | SchemaCacheErrorCode00
| SchemaCacheErrorCode01 | SchemaCacheErrorCode01
@@ -653,6 +661,7 @@ buildErrorCode code = "PGRST" <> case code of
ApiRequestErrorCode21 -> "121" ApiRequestErrorCode21 -> "121"
ApiRequestErrorCode22 -> "122" ApiRequestErrorCode22 -> "122"
ApiRequestErrorCode23 -> "123" ApiRequestErrorCode23 -> "123"
ApiRequestErrorCode24 -> "124"
SchemaCacheErrorCode00 -> "200" SchemaCacheErrorCode00 -> "200"
SchemaCacheErrorCode01 -> "201" SchemaCacheErrorCode01 -> "201"
+14 -2
View File
@@ -33,6 +33,8 @@ import qualified PostgREST.SchemaCache as SchemaCache
import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.ApiRequest.Preferences (PreferCount (..), import PostgREST.ApiRequest.Preferences (PreferCount (..),
PreferHandling (..),
PreferMaxAffected (..),
PreferTimezone (..), PreferTimezone (..),
PreferTransaction (..), PreferTransaction (..),
Preferences (..), Preferences (..),
@@ -114,9 +116,10 @@ createQuery mrPlan@MutateReadPlan{mrMedia} apiReq conf = do
pure resultSet pure resultSet
updateQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet updateQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
updateQuery mrPlan@MutateReadPlan{mrMedia} apiReq@ApiRequest{..} conf = do updateQuery mrPlan@MutateReadPlan{mrMedia} apiReq@ApiRequest{iPreferences=Preferences{..}, ..} conf = do
resultSet <- writeQuery mrPlan apiReq conf resultSet <- writeQuery mrPlan apiReq conf
failNotSingular mrMedia resultSet failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq optionalRollback conf apiReq
pure resultSet pure resultSet
@@ -141,9 +144,10 @@ failPut RSStandard{rsQueryTotal=queryTotal} =
throwError $ Error.ApiRequestError ApiRequestTypes.PutMatchingPkError throwError $ Error.ApiRequestError ApiRequestTypes.PutMatchingPkError
deleteQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet deleteQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
deleteQuery mrPlan@MutateReadPlan{mrMedia} apiReq@ApiRequest{..} conf = do deleteQuery mrPlan@MutateReadPlan{mrMedia} apiReq@ApiRequest{iPreferences=Preferences{..}, ..} conf = do
resultSet <- writeQuery mrPlan apiReq conf resultSet <- writeQuery mrPlan apiReq conf
failNotSingular mrMedia resultSet failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq optionalRollback conf apiReq
pure resultSet pure resultSet
@@ -165,6 +169,7 @@ invokeQuery rout CallReadPlan{..} apiReq@ApiRequest{iPreferences=Preferences{..}
optionalRollback conf apiReq optionalRollback conf apiReq
failNotSingular crMedia resultSet failNotSingular crMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
pure resultSet pure resultSet
openApiQuery :: SchemaCache -> PgVersion -> AppConfig -> Schema -> DbHandler (Maybe (TablesMap, RoutineMap, Maybe Text)) openApiQuery :: SchemaCache -> PgVersion -> AppConfig -> Schema -> DbHandler (Maybe (TablesMap, RoutineMap, Maybe Text))
@@ -213,6 +218,13 @@ failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
lift SQL.condemn lift SQL.condemn
throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
failExceedsMaxAffectedPref _ RSPlan{} = pure ()
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
lift SQL.condemn
throwError $ Error.ApiRequestError . ApiRequestTypes.MaxAffectedViolationError $ toInteger queryTotal
failsChangesOffLimits :: Maybe Integer -> ResultSet -> DbHandler () failsChangesOffLimits :: Maybe Integer -> ResultSet -> DbHandler ()
failsChangesOffLimits _ RSPlan{} = pure () failsChangesOffLimits _ RSPlan{} = pure ()
failsChangesOffLimits Nothing _ = pure () failsChangesOffLimits Nothing _ = pure ()
+6 -6
View File
@@ -74,7 +74,7 @@ readResponse WrappedReadPlan{wrMedia} headersOnly identifier ctxApiRequest@ApiRe
RSStandard{..} -> do RSStandard{..} -> do
let let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
headers = headers =
[ contentRange [ contentRange
, ( "Content-Location" , ( "Content-Location"
@@ -105,7 +105,7 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan, mrMedia} ctx
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;} pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
prefHeader = prefAppliedHeader $ prefHeader = prefAppliedHeader $
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution) Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone [] preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing []
headers = headers =
catMaybes catMaybes
[ if null rsLocation then [ if null rsLocation then
@@ -146,7 +146,7 @@ updateResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Pre
contentRangeHeader = contentRangeHeader =
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $ Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone [] prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
headers = catMaybes [contentRangeHeader, prefHeader] headers = catMaybes [contentRangeHeader, prefHeader]
let (status, headers', body) = let (status, headers', body) =
@@ -166,7 +166,7 @@ singleUpsertResponse :: MutateReadPlan -> ApiRequest -> ResultSet -> Either Erro
singleUpsertResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet = case resultSet of singleUpsertResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
let let
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
cTHeader = contentTypeHeaders mrMedia ctxApiRequest cTHeader = contentTypeHeaders mrMedia ctxApiRequest
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200 let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
@@ -190,7 +190,7 @@ deleteResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Pre
contentRangeHeader = contentRangeHeader =
RangeQuery.contentRangeH 1 0 $ RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
headers = contentRangeHeader : prefHeader headers = contentRangeHeader : prefHeader
let (status, headers', body) = let (status, headers', body) =
@@ -243,7 +243,7 @@ invokeResponse CallReadPlan{crMedia} invMethod proc ctxApiRequest@ApiRequest{iPr
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) $ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
else LBS.fromStrict rsBody else LBS.fromStrict rsBody
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
headers = contentRange : prefHeader headers = contentRange : prefHeader
let (status', headers', body) = let (status', headers', body) =
@@ -119,3 +119,75 @@ spec =
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [matchContentTypeJson , matchHeaders = [matchContentTypeJson
, "Preference-Applied" <:> "handling=lenient"]} , "Preference-Applied" <:> "handling=lenient"]}
context "test Prefer: max-affected with handling=strict" $ do
it "should fail if items deleted more than 10" $
request methodDelete "/items?id=lt.15"
[("Prefer", "handling=strict, max-affected=10")]
""
`shouldRespondWith`
[json|{"code":"PGRST124","details":"The query affects 14 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"}|]
{ matchStatus = 400 }
it "should succeed if items deleted less than 10" $
request methodDelete "/items?id=lt.10"
[("Prefer", "handling=strict, max-affected=10")]
""
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=strict, max-affected=10"]}
it "should fail if items updated more than 0" $
request methodPatch "/tiobe_pls?name=eq.Java"
[("Prefer", "handling=strict, max-affected=0")]
[json| [{"name":"Java", "rank":19}] |]
`shouldRespondWith`
[json|{"code":"PGRST124","details":"The query affects 1 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"}|]
{ matchStatus = 400 }
it "should succeed if items updated equal 1" $
request methodDelete "/tiobe_pls?name=eq.Java"
[("Prefer", "handling=strict, max-affected=1")]
[json| [{"name":"Java", "rank":19}] |]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=strict, max-affected=1"]}
context "test Prefer: max-affected with handling=lenient" $ do
it "should not fail" $
request methodDelete "/items?id=lt.15"
[("Prefer", "handling=lenient, max-affected=10")]
""
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
it "should succeed if items deleted less than 10" $
request methodDelete "/items?id=lt.10"
[("Prefer", "handling=lenient, max-affected=10")]
""
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
it "should not fail" $
request methodPatch "/tiobe_pls?name=eq.Java"
[("Prefer", "handling=lenient, max-affected=0")]
[json| [{"name":"Java", "rank":19}] |]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
it "should succeed if items updated equal 1" $
request methodDelete "/tiobe_pls?name=eq.Java"
[("Prefer", "handling=lenient, max-affected=1")]
[json| [{"name":"Java", "rank":19}] |]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}