feat: add timezone in Prefer header (#3024)

* increase reloading timeout in io-tests
* increase memory test by 1M
This commit is contained in:
Taimoor Zaeem
2023-11-13 14:16:27 -05:00
committed by GitHub
parent f10d4139fe
commit 3c1a7f2641
14 changed files with 142 additions and 40 deletions
+2 -1
View File
@@ -15,7 +15,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2983, Add more data to `Server-Timing` header - @develop7 - #2983, Add more data to `Server-Timing` header - @develop7
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem - #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem
- #2825, SQL handlers for custom media types - @steve-chavez - #2825, SQL handlers for custom media types - @steve-chavez
- Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901 + Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901
- #2799, Add timezone in Prefer header - @taimoorzaeem
### Fixed ### Fixed
+1 -1
View File
@@ -72,7 +72,7 @@ let
# We try to make the database cluster as independent as possible from the host # We try to make the database cluster as independent as possible from the host
# by specifying the timezone, locale and encoding. # by specifying the timezone, locale and encoding.
# initdb -U creates a superuser(man initdb) # initdb -U creates a superuser(man initdb)
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \ TZ=$PGTZ initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
>> "$setuplog" >> "$setuplog"
log "Starting the database cluster..." log "Starting the database cluster..."
+4 -3
View File
@@ -55,6 +55,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
convertToLimitZeroRange, convertToLimitZeroRange,
hasLimitZero, hasLimitZero,
rangeRequested) rangeRequested)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
Schema) Schema)
@@ -134,8 +135,8 @@ data ApiRequest = ApiRequest {
} }
-- | Examines HTTP request and translates it into user intent. -- | Examines HTTP request and translates it into user intent.
userApiRequest :: AppConfig -> Request -> RequestBody -> Either ApiRequestError ApiRequest userApiRequest :: AppConfig -> Request -> RequestBody -> SchemaCache -> Either ApiRequestError ApiRequest
userApiRequest conf req reqBody = do userApiRequest conf req reqBody sCache = do
pInfo@PathInfo{..} <- getPathInfo conf $ pathInfo req pInfo@PathInfo{..} <- getPathInfo conf $ pathInfo req
act <- getAction pInfo method act <- getAction pInfo method
qPrms <- first QueryParamError $ QueryParams.parse (pathIsProc && act `elem` [ActionInvoke InvGet, ActionInvoke InvHead]) $ rawQueryString req qPrms <- first QueryParamError $ QueryParams.parse (pathIsProc && act `elem` [ActionInvoke InvGet, ActionInvoke InvHead]) $ rawQueryString req
@@ -150,7 +151,7 @@ userApiRequest conf req reqBody = do
, iRange = ranges , iRange = ranges
, iTopLevelRange = topLevelRange , iTopLevelRange = topLevelRange
, iPayload = payload , iPayload = payload
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) hdrs , iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) (dbTimezones sCache) hdrs
, iQueryParams = qPrms , iQueryParams = qPrms
, iColumns = columns , iColumns = columns
, iHeaders = iHdrs , iHeaders = iHdrs
+37 -12
View File
@@ -10,12 +10,13 @@
module PostgREST.ApiRequest.Preferences module PostgREST.ApiRequest.Preferences
( Preferences(..) ( Preferences(..)
, PreferCount(..) , PreferCount(..)
, PreferHandling(..)
, PreferMissing(..) , PreferMissing(..)
, PreferParameters(..) , PreferParameters(..)
, PreferRepresentation(..) , PreferRepresentation(..)
, PreferResolution(..) , PreferResolution(..)
, PreferTransaction(..) , PreferTransaction(..)
, PreferHandling(..) , PreferTimezone(..)
, fromHeaders , fromHeaders
, shouldCount , shouldCount
, prefAppliedHeader , prefAppliedHeader
@@ -23,8 +24,11 @@ module PostgREST.ApiRequest.Preferences
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as S
import qualified Network.HTTP.Types.Header as HTTP import qualified Network.HTTP.Types.Header as HTTP
import PostgREST.Config.Database (TimezoneNames)
import Protolude import Protolude
-- $setup -- $setup
@@ -37,6 +41,7 @@ import Protolude
-- >>> deriving instance Show PreferTransaction -- >>> deriving instance Show PreferTransaction
-- >>> deriving instance Show PreferMissing -- >>> deriving instance Show PreferMissing
-- >>> deriving instance Show PreferHandling -- >>> deriving instance Show PreferHandling
-- >>> deriving instance Show PreferTimezone
-- >>> deriving instance Show Preferences -- >>> deriving instance Show Preferences
-- | Preferences recognized by the application. -- | Preferences recognized by the application.
@@ -49,15 +54,17 @@ data Preferences
, preferTransaction :: Maybe PreferTransaction , preferTransaction :: Maybe PreferTransaction
, preferMissing :: Maybe PreferMissing , preferMissing :: Maybe PreferMissing
, preferHandling :: Maybe PreferHandling , preferHandling :: Maybe PreferHandling
, preferTimezone :: Maybe PreferTimezone
, invalidPrefs :: [ByteString] , invalidPrefs :: [ByteString]
} }
-- | -- |
-- Parse HTTP headers based on RFC7240[1] to identify preferences. -- Parse HTTP headers based on RFC7240[1] to identify preferences.
-- --
-- One header with comma-separated values can be used to set multiple preferences: -- >>> let sc = S.fromList ["America/Los_Angeles"]
-- --
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact")] -- 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")]
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -66,12 +73,14 @@ data Preferences
-- , preferTransaction = Nothing -- , preferTransaction = Nothing
-- , preferMissing = Nothing -- , preferMissing = Nothing
-- , preferHandling = Nothing -- , preferHandling = Nothing
-- , preferTimezone = Just
-- ( PreferTimezone "America/Los_Angeles" )
-- , invalidPrefs = [] -- , invalidPrefs = []
-- } -- }
-- --
-- Multiple headers can also be used: -- Multiple headers can also be used:
-- --
-- >>> pPrint $ fromHeaders True [("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")]
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -80,18 +89,19 @@ data Preferences
-- , preferTransaction = Nothing -- , preferTransaction = Nothing
-- , preferMissing = Just ApplyNulls -- , preferMissing = Just ApplyNulls
-- , preferHandling = Just Lenient -- , preferHandling = Just Lenient
-- , preferTimezone = Nothing
-- , invalidPrefs = [ "invalid" ] -- , invalidPrefs = [ "invalid" ]
-- } -- }
-- --
-- If a preference is set more than once, only the first is used: -- If a preference is set more than once, only the first is used:
-- --
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")] -- >>> preferTransaction $ fromHeaders True sc [("Prefer", "tx=commit, tx=rollback")]
-- Just Commit -- Just Commit
-- --
-- This is also the case across multiple headers: -- This is also the case across multiple headers:
-- --
-- >>> :{ -- >>> :{
-- preferResolution . fromHeaders True $ -- preferResolution . fromHeaders True sc $
-- [ ("Prefer", "resolution=ignore-duplicates") -- [ ("Prefer", "resolution=ignore-duplicates")
-- , ("Prefer", "resolution=merge-duplicates") -- , ("Prefer", "resolution=merge-duplicates")
-- ] -- ]
@@ -101,7 +111,7 @@ data Preferences
-- --
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized: -- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
-- --
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")] -- >>> pPrint $ fromHeaders True sc [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- Preferences -- Preferences
-- { preferResolution = Nothing -- { preferResolution = Nothing
-- , preferRepresentation = Just Full -- , preferRepresentation = Just Full
@@ -110,20 +120,22 @@ data Preferences
-- , preferTransaction = Just Commit -- , preferTransaction = Just Commit
-- , preferMissing = Just ApplyDefaults -- , preferMissing = Just ApplyDefaults
-- , preferHandling = Just Strict -- , preferHandling = Just Strict
-- , preferTimezone = Nothing
-- , invalidPrefs = [ "anything" ] -- , invalidPrefs = [ "anything" ]
-- } -- }
-- --
fromHeaders :: Bool -> [HTTP.Header] -> Preferences fromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences
fromHeaders allowTxEndOverride headers = fromHeaders allowTxDbOverride acceptedTzNames headers =
Preferences Preferences
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates] { preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
, preferRepresentation = parsePrefs [Full, None, HeadersOnly] , preferRepresentation = parsePrefs [Full, None, HeadersOnly]
, preferParameters = parsePrefs [SingleObject] , preferParameters = parsePrefs [SingleObject]
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount] , preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing , preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls] , preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient] , preferHandling = parsePrefs [Strict, Lenient]
, invalidPrefs = filter (`notElem` acceptedPrefs) prefs , preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
, invalidPrefs = filter checkPrefs prefs
} }
where where
mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString] mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]
@@ -139,6 +151,11 @@ fromHeaders allowTxEndOverride 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
isTimezonePrefAccepted = (S.member <$> timezonePref <*> pure acceptedTzNames) == Just True
checkPrefs p = p `notElem` acceptedPrefs && not isTimezonePrefAccepted
parsePrefs :: ToHeaderValue a => [a] -> Maybe a parsePrefs :: ToHeaderValue a => [a] -> Maybe a
parsePrefs vals = parsePrefs vals =
head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs
@@ -147,7 +164,7 @@ fromHeaders allowTxEndOverride 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 } = prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone } =
if null prefsVals if null prefsVals
then Nothing then Nothing
else Just (HTTP.hPreferenceApplied, combined) else Just (HTTP.hPreferenceApplied, combined)
@@ -161,6 +178,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
, toHeaderValue <$> preferCount , toHeaderValue <$> preferCount
, toHeaderValue <$> preferTransaction , toHeaderValue <$> preferTransaction
, toHeaderValue <$> preferHandling , toHeaderValue <$> preferHandling
, toHeaderValue <$> preferTimezone
] ]
-- | -- |
@@ -253,3 +271,10 @@ data PreferHandling
instance ToHeaderValue PreferHandling where instance ToHeaderValue PreferHandling where
toHeaderValue Strict = "handling=strict" toHeaderValue Strict = "handling=strict"
toHeaderValue Lenient = "handling=lenient" toHeaderValue Lenient = "handling=lenient"
-- |
-- Change timezone
newtype PreferTimezone = PreferTimezone ByteString
instance ToHeaderValue PreferTimezone where
toHeaderValue (PreferTimezone tz) = "timezone=" <> tz
+1 -1
View File
@@ -157,7 +157,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
apiRequest <- apiRequest <-
liftEither . mapLeft Error.ApiRequestError $ liftEither . mapLeft Error.ApiRequestError $
ApiRequest.userApiRequest conf req body ApiRequest.userApiRequest conf req body sCache
let jwtTiming = (SMJwt, if configDbPlanEnabled then Auth.getJwtDur req else Nothing) let jwtTiming = (SMJwt, if configDbPlanEnabled then Auth.getJwtDur req else Nothing)
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache jwtTiming handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache jwtTiming
+3 -1
View File
@@ -3,10 +3,11 @@
module PostgREST.Config.Database module PostgREST.Config.Database
( pgVersionStatement ( pgVersionStatement
, queryDbSettings , queryDbSettings
, queryRoleSettings
, queryPgVersion , queryPgVersion
, queryRoleSettings
, RoleSettings , RoleSettings
, RoleIsolationLvl , RoleIsolationLvl
, TimezoneNames
, toIsolationLevel , toIsolationLevel
) where ) where
@@ -29,6 +30,7 @@ import Protolude
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString)) type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
type TimezoneNames = Set ByteString -- cache timezone names for prefer timezone=
toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel
toIsolationLevel a = case a of toIsolationLevel a = case a of
+9 -7
View File
@@ -33,6 +33,7 @@ import qualified PostgREST.SchemaCache as SchemaCache
import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.ApiRequest.Preferences (PreferCount (..), import PostgREST.ApiRequest.Preferences (PreferCount (..),
PreferTimezone (..),
PreferTransaction (..), PreferTransaction (..),
Preferences (..), Preferences (..),
shouldCount) shouldCount)
@@ -235,21 +236,22 @@ optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
-- | Runs local (transaction scoped) GUCs for every request. -- | Runs local (transaction scoped) GUCs for every request.
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] -> setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] ->
ApiRequest -> DbHandler () ApiRequest -> DbHandler ()
setPgLocals AppConfig{..} claims role roleSettings req = lift $ setPgLocals AppConfig{..} claims role roleSettings ApiRequest{..} = lift $
SQL.statement mempty $ SQL.dynamicallyParameterized SQL.statement mempty $ SQL.dynamicallyParameterized
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ roleSettingsSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql)) ("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ roleSettingsSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ appSettingsSql))
HD.noResult configDbPreparedStatements HD.noResult configDbPreparedStatements
where where
methodSql = setConfigWithConstantName ("request.method", iMethod req) methodSql = setConfigWithConstantName ("request.method", iMethod)
pathSql = setConfigWithConstantName ("request.path", iPath req) pathSql = setConfigWithConstantName ("request.path", iPath)
headersSql = setConfigWithConstantNameJSON "request.headers" (iHeaders req) headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
cookiesSql = setConfigWithConstantNameJSON "request.cookies" (iCookies req) cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)] claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
roleSql = [setConfigWithConstantName ("role", role)] roleSql = [setConfigWithConstantName ("role", role)]
roleSettingsSql = setConfigWithDynamicName <$> roleSettings roleSettingsSql = setConfigWithDynamicName <$> roleSettings
appSettingsSql = setConfigWithDynamicName <$> (join bimap toUtf8 <$> configAppSettings) appSettingsSql = setConfigWithDynamicName <$> (join bimap toUtf8 <$> configAppSettings)
timezoneSql = maybe mempty (\(PreferTimezone tz) -> [setConfigWithConstantName ("timezone", tz)]) $ preferTimezone iPreferences
searchPathSql = searchPathSql =
let schemas = escapeIdentList (iSchema req : configDbExtraSearchPath) in let schemas = escapeIdentList (iSchema : configDbExtraSearchPath) in
setConfigWithConstantName ("search_path", schemas) setConfigWithConstantName ("search_path", schemas)
-- | Runs the pre-request function. -- | Runs the pre-request function.
+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 [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone []
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 [] preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone []
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 [] prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone []
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 [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone []
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 [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone []
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 [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone []
headers = contentRange : prefHeader headers = contentRange : prefHeader
let (status', headers', body) = let (status', headers', body) =
+16 -2
View File
@@ -43,7 +43,8 @@ import Contravariant.Extras (contrazip2)
import Text.InterpolatedString.Perl6 (q) import Text.InterpolatedString.Perl6 (q)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Config.Database (pgVersionStatement, import PostgREST.Config.Database (TimezoneNames,
pgVersionStatement,
toIsolationLevel) toIsolationLevel)
import PostgREST.Config.PgVersion (PgVersion, pgVersion100, import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
pgVersion110, pgVersion110,
@@ -80,14 +81,17 @@ data SchemaCache = SchemaCache
, dbRoutines :: RoutineMap , dbRoutines :: RoutineMap
, dbRepresentations :: RepresentationsMap , dbRepresentations :: RepresentationsMap
, dbMediaHandlers :: MediaHandlerMap , dbMediaHandlers :: MediaHandlerMap
, dbTimezones :: TimezoneNames
} }
instance JSON.ToJSON SchemaCache where instance JSON.ToJSON SchemaCache where
toJSON (SchemaCache tabs rels routs reps _) = JSON.object [ toJSON (SchemaCache tabs rels routs reps _ _) = JSON.object [
"dbTables" .= JSON.toJSON tabs "dbTables" .= JSON.toJSON tabs
, "dbRelationships" .= JSON.toJSON rels , "dbRelationships" .= JSON.toJSON rels
, "dbRoutines" .= JSON.toJSON routs , "dbRoutines" .= JSON.toJSON routs
, "dbRepresentations" .= JSON.toJSON reps , "dbRepresentations" .= JSON.toJSON reps
, "dbMediaHandlers" .= JSON.emptyArray , "dbMediaHandlers" .= JSON.emptyArray
, "dbTimezones" .= JSON.emptyArray
] ]
-- | A view foreign key or primary key dependency detected on its source table -- | A view foreign key or primary key dependency detected on its source table
@@ -140,6 +144,7 @@ querySchemaCache AppConfig{..} = do
cRels <- SQL.statement mempty $ allComputedRels prepared cRels <- SQL.statement mempty $ allComputedRels prepared
reps <- SQL.statement schemas $ dataRepresentations prepared reps <- SQL.statement schemas $ dataRepresentations prepared
mHdlers <- SQL.statement schemas $ mediaHandlers pgVer prepared mHdlers <- SQL.statement schemas $ mediaHandlers pgVer prepared
tzones <- SQL.statement mempty $ timezones prepared
_ <- _ <-
let sleepCall = SQL.Statement "select pg_sleep($1)" (param HE.int4) HD.noResult prepared in let sleepCall = SQL.Statement "select pg_sleep($1)" (param HE.int4) HD.noResult prepared in
whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing
@@ -153,6 +158,7 @@ querySchemaCache AppConfig{..} = do
, dbRoutines = funcs , dbRoutines = funcs
, dbRepresentations = reps , dbRepresentations = reps
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones , dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
, dbTimezones = tzones
} }
where where
schemas = toList configDbSchemas schemas = toList configDbSchemas
@@ -188,6 +194,7 @@ removeInternal schemas dbStruct =
, dbRoutines = dbRoutines dbStruct -- procs are only obtained from the exposed schemas, no need to filter them. , dbRoutines = dbRoutines dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
, dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API , dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API
, dbMediaHandlers = dbMediaHandlers dbStruct , dbMediaHandlers = dbMediaHandlers dbStruct
, dbTimezones = dbTimezones dbStruct
} }
where where
hasInternalJunction ComputedRelationship{} = False hasInternalJunction ComputedRelationship{} = False
@@ -1178,6 +1185,13 @@ decodeMediaHandlers =
<*> (QualifiedIdentifier <$> column HD.text <*> column HD.text) <*> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text) <*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
timezones :: Bool -> SQL.Statement () TimezoneNames
timezones = SQL.Statement sql HE.noParams decodeTimezones
where
sql = "SELECT name FROM pg_timezone_names"
decodeTimezones :: HD.Result TimezoneNames
decodeTimezones = S.fromList . map encodeUtf8 <$> HD.rowList (column HD.text)
param :: HE.Value a -> HE.Params a param :: HE.Value a -> HE.Params a
param = HE.param . HE.nonNullable param = HE.param . HE.nonNullable
+3 -3
View File
@@ -19,17 +19,17 @@ from config import *
def sleep_until_postgrest_scache_reload(): def sleep_until_postgrest_scache_reload():
"Sleep until schema cache reload" "Sleep until schema cache reload"
time.sleep(0.2) time.sleep(0.3)
def sleep_until_postgrest_config_reload(): def sleep_until_postgrest_config_reload():
"Sleep until config reload" "Sleep until config reload"
time.sleep(0.1) time.sleep(0.2)
def sleep_until_postgrest_full_reload(): def sleep_until_postgrest_full_reload():
"Sleep until schema cache plus config reload" "Sleep until schema cache plus config reload"
time.sleep(0.2) time.sleep(0.3)
class PostgrestTimedOut(Exception): class PostgrestTimedOut(Exception):
+2 -2
View File
@@ -114,8 +114,8 @@ jsonKeyTest "50M" "POST" "/rpc/leak?columns=blob" "172M"
jsonKeyTest "50M" "POST" "/leak?columns=blob" "172M" jsonKeyTest "50M" "POST" "/leak?columns=blob" "172M"
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "172M" jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "172M"
postJsonArrayTest "1000" "/perf_articles?columns=id,body" "14M" postJsonArrayTest "1000" "/perf_articles?columns=id,body" "15M"
postJsonArrayTest "10000" "/perf_articles?columns=id,body" "14M" postJsonArrayTest "10000" "/perf_articles?columns=id,body" "15M"
postJsonArrayTest "100000" "/perf_articles?columns=id,body" "24M" postJsonArrayTest "100000" "/perf_articles?columns=id,body" "24M"
trap - int term exit trap - int term exit
+49 -1
View File
@@ -12,7 +12,7 @@ import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
describe "check prefer: handling=strict and handling=lenient" $ do describe "test prefer headers and preference-applied headers" $ do
context "check behaviour of Prefer: handling=strict" $ do context "check behaviour of Prefer: handling=strict" $ do
it "throws error when handling=strict and invalid prefs are given" $ it "throws error when handling=strict and invalid prefs are given" $
@@ -71,3 +71,51 @@ spec =
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
context "test Prefer: timezone=America/Los_Angeles" $ do
it "should change timezone with handling=strict" $
request methodGet "/timestamps"
[("Prefer", "handling=strict, timezone=America/Los_Angeles")]
""
`shouldRespondWith`
[json|[{"t":"2023-10-18T05:37:59.611-07:00"}, {"t":"2023-10-18T07:37:59.611-07:00"}, {"t":"2023-10-18T09:37:59.611-07:00"}]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson
, "Preference-Applied" <:> "handling=strict, timezone=America/Los_Angeles"]}
it "should change timezone without handling=strict" $
request methodGet "/timestamps"
[("Prefer", "timezone=America/Los_Angeles")]
""
`shouldRespondWith`
[json|[{"t":"2023-10-18T05:37:59.611-07:00"}, {"t":"2023-10-18T07:37:59.611-07:00"}, {"t":"2023-10-18T09:37:59.611-07:00"}]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson
, "Preference-Applied" <:> "timezone=America/Los_Angeles"] }
context "test Prefer: timezone=Invalid/Timezone" $ do
it "should throw error with handling=strict" $
request methodGet "/timestamps"
[("Prefer", "handling=strict, timezone=Invalid/Timezone")]
""
`shouldRespondWith`
[json|{"code":"PGRST122","details":"Invalid preferences: timezone=Invalid/Timezone","hint":null,"message":"Invalid preferences given with handling=strict"}|]
{ matchStatus = 400 }
it "should return with default timezone without handling or with handling=lenient" $ do
request methodGet "/timestamps"
[("Prefer", "timezone=Invalid/Timezone")]
""
`shouldRespondWith`
[json|[{"t":"2023-10-18T12:37:59.611+00:00"}, {"t":"2023-10-18T14:37:59.611+00:00"}, {"t":"2023-10-18T16:37:59.611+00:00"}]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]}
request methodGet "/timestamps"
[("Prefer", "handling=lenient, timezone=Invalid/Timezone")]
""
`shouldRespondWith`
[json|[{"t":"2023-10-18T12:37:59.611+00:00"}, {"t":"2023-10-18T14:37:59.611+00:00"}, {"t":"2023-10-18T16:37:59.611+00:00"}]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson
, "Preference-Applied" <:> "handling=lenient"]}
+5
View File
@@ -861,3 +861,8 @@ INSERT INTO table_b(table_a_id, name) VALUES (1, 'Test 1'), (2, 'Test 2'), (null
TRUNCATE TABLE lines CASCADE; TRUNCATE TABLE lines CASCADE;
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::extensions.geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::extensions.geometry); insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::extensions.geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::extensions.geometry);
TRUNCATE TABLE timestamps CASCADE;
INSERT INTO timestamps VALUES ('2023-10-18 12:37:59.611000+0000');
INSERT INTO timestamps VALUES ('2023-10-18 14:37:59.611000+0000');
INSERT INTO timestamps VALUES ('2023-10-18 16:37:59.611000+0000');
+4
View File
@@ -3627,3 +3627,7 @@ create aggregate test.bom_csv_agg (test.lines) (
); );
create table empty_string as select 1 as id, ''::text as string; create table empty_string as select 1 as id, ''::text as string;
create table timestamps (
t timestamp with time zone
);