feat: add timezone in Prefer header (#3024)
* increase reloading timeout in io-tests * increase memory test by 1M
This commit is contained in:
+2
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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..."
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]}
|
||||||
|
|||||||
Vendored
+5
@@ -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');
|
||||||
|
|||||||
Vendored
+4
@@ -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
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user