refactor: Move accept content type code from App into ApiRequest (#1830)
This commit is contained in:
+27
-51
@@ -89,7 +89,6 @@ data RequestContext = RequestContext
|
|||||||
{ ctxConfig :: AppConfig
|
{ ctxConfig :: AppConfig
|
||||||
, ctxDbStructure :: DbStructure
|
, ctxDbStructure :: DbStructure
|
||||||
, ctxApiRequest :: ApiRequest
|
, ctxApiRequest :: ApiRequest
|
||||||
, ctxContentType :: ContentType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler = ExceptT Error
|
type Handler = ExceptT Error
|
||||||
@@ -161,7 +160,7 @@ postgrestResponse
|
|||||||
-> UTCTime
|
-> UTCTime
|
||||||
-> Wai.Request
|
-> Wai.Request
|
||||||
-> Handler IO Wai.Response
|
-> Handler IO Wai.Response
|
||||||
postgrestResponse conf@AppConfig{..} maybeDbStructure pool time req = do
|
postgrestResponse conf maybeDbStructure pool time req = do
|
||||||
body <- lift $ Wai.strictRequestBody req
|
body <- lift $ Wai.strictRequestBody req
|
||||||
|
|
||||||
dbStructure <-
|
dbStructure <-
|
||||||
@@ -173,21 +172,14 @@ postgrestResponse conf@AppConfig{..} maybeDbStructure pool time req = do
|
|||||||
|
|
||||||
apiRequest@ApiRequest{..} <-
|
apiRequest@ApiRequest{..} <-
|
||||||
liftEither . mapLeft Error.ApiRequestError $
|
liftEither . mapLeft Error.ApiRequestError $
|
||||||
ApiRequest.userApiRequest configDbSchemas configDbRootSpec dbStructure req body
|
ApiRequest.userApiRequest conf dbStructure req body
|
||||||
|
|
||||||
-- The JWT must be checked before touching the db
|
-- The JWT must be checked before touching the db
|
||||||
jwtClaims <- Auth.jwtClaims conf (toS iJWT) time
|
jwtClaims <- Auth.jwtClaims conf (toS iJWT) time
|
||||||
|
|
||||||
contentType <-
|
|
||||||
case ApiRequest.mutuallyAgreeable (requestContentTypes conf apiRequest) iAccepts of
|
|
||||||
Just ct ->
|
|
||||||
return ct
|
|
||||||
Nothing ->
|
|
||||||
throwError . Error.ContentTypeError $ map ContentType.toMime iAccepts
|
|
||||||
|
|
||||||
let
|
let
|
||||||
handleReq apiReq =
|
handleReq apiReq =
|
||||||
handleRequest $ RequestContext conf dbStructure apiReq contentType
|
handleRequest $ RequestContext conf dbStructure apiReq
|
||||||
|
|
||||||
runDbHandler pool (txMode apiRequest) jwtClaims .
|
runDbHandler pool (txMode apiRequest) jwtClaims .
|
||||||
Middleware.optionalRollback conf apiRequest $
|
Middleware.optionalRollback conf apiRequest $
|
||||||
@@ -205,7 +197,7 @@ runDbHandler pool mode jwtClaims handler = do
|
|||||||
liftEither resp
|
liftEither resp
|
||||||
|
|
||||||
handleRequest :: RequestContext -> DbHandler Wai.Response
|
handleRequest :: RequestContext -> DbHandler Wai.Response
|
||||||
handleRequest context@(RequestContext _ _ ApiRequest{..} _) =
|
handleRequest context@(RequestContext _ _ ApiRequest{..}) =
|
||||||
case (iAction, iTarget) of
|
case (iAction, iTarget) of
|
||||||
(ActionRead headersOnly, TargetIdent identifier) ->
|
(ActionRead headersOnly, TargetIdent identifier) ->
|
||||||
handleRead headersOnly identifier context
|
handleRead headersOnly identifier context
|
||||||
@@ -246,9 +238,9 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
else
|
else
|
||||||
countQuery
|
countQuery
|
||||||
)
|
)
|
||||||
(ctxContentType == CTSingularJSON)
|
(iAcceptContentType == CTSingularJSON)
|
||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
(ctxContentType == CTTextCSV)
|
(iAcceptContentType == CTTextCSV)
|
||||||
bField
|
bField
|
||||||
(pgVersion ctxDbStructure)
|
(pgVersion ctxDbStructure)
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
@@ -268,7 +260,7 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
]
|
]
|
||||||
++ contentTypeHeaders context
|
++ contentTypeHeaders context
|
||||||
|
|
||||||
failNotSingular ctxContentType queryTotal . response status headers $
|
failNotSingular iAcceptContentType queryTotal . response status headers $
|
||||||
if headersOnly then mempty else toS body
|
if headersOnly then mempty else toS body
|
||||||
|
|
||||||
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
|
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
|
||||||
@@ -317,14 +309,14 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
|||||||
(\x -> ("Preference-Applied", BS8.pack $ show x)) <$> iPreferResolution
|
(\x -> ("Preference-Applied", BS8.pack $ show x)) <$> iPreferResolution
|
||||||
]
|
]
|
||||||
|
|
||||||
failNotSingular ctxContentType resQueryTotal $
|
failNotSingular iAcceptContentType resQueryTotal $
|
||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status201 (headers ++ contentTypeHeaders context) (toS resBody)
|
response HTTP.status201 (headers ++ contentTypeHeaders context) (toS resBody)
|
||||||
else
|
else
|
||||||
response HTTP.status201 headers mempty
|
response HTTP.status201 headers mempty
|
||||||
|
|
||||||
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
||||||
handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} contentType) = do
|
handleUpdate identifier context@(RequestContext _ _ ApiRequest{..}) = do
|
||||||
WriteQueryResult{..} <- writeQuery identifier False mempty context
|
WriteQueryResult{..} <- writeQuery identifier False mempty context
|
||||||
|
|
||||||
let
|
let
|
||||||
@@ -339,14 +331,14 @@ handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} contentType)
|
|||||||
RangeQuery.contentRangeH 0 (resQueryTotal - 1) $
|
RangeQuery.contentRangeH 0 (resQueryTotal - 1) $
|
||||||
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
||||||
|
|
||||||
failNotSingular contentType resQueryTotal $
|
failNotSingular iAcceptContentType resQueryTotal $
|
||||||
if fullRepr then
|
if fullRepr then
|
||||||
response status (contentTypeHeaders context ++ [contentRangeHeader]) (toS resBody)
|
response status (contentTypeHeaders context ++ [contentRangeHeader]) (toS resBody)
|
||||||
else
|
else
|
||||||
response status [contentRangeHeader] mempty
|
response status [contentRangeHeader] mempty
|
||||||
|
|
||||||
handleSingleUpsert :: QualifiedIdentifier -> RequestContext-> DbHandler Wai.Response
|
handleSingleUpsert :: QualifiedIdentifier -> RequestContext-> DbHandler Wai.Response
|
||||||
handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..}) = do
|
||||||
when (iTopLevelRange /= RangeQuery.allRange) $
|
when (iTopLevelRange /= RangeQuery.allRange) $
|
||||||
throwError Error.PutRangeNotAllowedError
|
throwError Error.PutRangeNotAllowedError
|
||||||
|
|
||||||
@@ -370,7 +362,7 @@ handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
response HTTP.status204 (contentTypeHeaders context) mempty
|
response HTTP.status204 (contentTypeHeaders context) mempty
|
||||||
|
|
||||||
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
||||||
handleDelete identifier context@(RequestContext _ _ ApiRequest{..} contentType) = do
|
handleDelete identifier context@(RequestContext _ _ ApiRequest{..}) = do
|
||||||
WriteQueryResult{..} <- writeQuery identifier False mempty context
|
WriteQueryResult{..} <- writeQuery identifier False mempty context
|
||||||
|
|
||||||
let
|
let
|
||||||
@@ -379,7 +371,7 @@ handleDelete identifier context@(RequestContext _ _ ApiRequest{..} contentType)
|
|||||||
RangeQuery.contentRangeH 1 0 $
|
RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
||||||
|
|
||||||
failNotSingular contentType resQueryTotal $
|
failNotSingular iAcceptContentType resQueryTotal $
|
||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200
|
response HTTP.status200
|
||||||
(contentTypeHeaders context ++ [contentRangeHeader])
|
(contentTypeHeaders context ++ [contentRangeHeader])
|
||||||
@@ -443,8 +435,8 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(QueryBuilder.readRequestToQuery req)
|
(QueryBuilder.readRequestToQuery req)
|
||||||
(QueryBuilder.readRequestToCountQuery req)
|
(QueryBuilder.readRequestToCountQuery req)
|
||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
(ctxContentType == CTSingularJSON)
|
(iAcceptContentType == CTSingularJSON)
|
||||||
(ctxContentType == CTTextCSV)
|
(iAcceptContentType == CTTextCSV)
|
||||||
(iPreferParameters == Just MultipleObjects)
|
(iPreferParameters == Just MultipleObjects)
|
||||||
bField
|
bField
|
||||||
(pgVersion ctxDbStructure)
|
(pgVersion ctxDbStructure)
|
||||||
@@ -456,13 +448,13 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(status, contentRange) =
|
(status, contentRange) =
|
||||||
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
|
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
|
||||||
|
|
||||||
failNotSingular ctxContentType queryTotal $
|
failNotSingular iAcceptContentType queryTotal $
|
||||||
response status
|
response status
|
||||||
(contentTypeHeaders context ++ [contentRange])
|
(contentTypeHeaders context ++ [contentRange])
|
||||||
(if invMethod == InvHead then mempty else toS body)
|
(if invMethod == InvHead then mempty else toS body)
|
||||||
|
|
||||||
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
|
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
|
||||||
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do
|
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest) = do
|
||||||
body <-
|
body <-
|
||||||
lift $
|
lift $
|
||||||
OpenAPI.encode conf dbStructure
|
OpenAPI.encode conf dbStructure
|
||||||
@@ -519,9 +511,9 @@ writeQuery identifier@QualifiedIdentifier{..} isInsert pkCols context@RequestCon
|
|||||||
Statements.createWriteStatement
|
Statements.createWriteStatement
|
||||||
(QueryBuilder.readRequestToQuery readReq)
|
(QueryBuilder.readRequestToQuery readReq)
|
||||||
(QueryBuilder.mutateRequestToQuery mutateReq)
|
(QueryBuilder.mutateRequestToQuery mutateReq)
|
||||||
(ctxContentType == CTSingularJSON)
|
(iAcceptContentType ctxApiRequest == CTSingularJSON)
|
||||||
isInsert
|
isInsert
|
||||||
(ctxContentType == CTTextCSV)
|
(iAcceptContentType ctxApiRequest == CTTextCSV)
|
||||||
(iPreferRepresentation ctxApiRequest)
|
(iPreferRepresentation ctxApiRequest)
|
||||||
pkCols
|
pkCols
|
||||||
(pgVersion ctxDbStructure)
|
(pgVersion ctxDbStructure)
|
||||||
@@ -562,7 +554,7 @@ returnsScalar (TargetProc proc _) = Proc.procReturnsScalar proc
|
|||||||
returnsScalar _ = False
|
returnsScalar _ = False
|
||||||
|
|
||||||
readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
|
readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
|
||||||
readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest _) =
|
readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest) =
|
||||||
liftEither $
|
liftEither $
|
||||||
ReqBuilder.readRequest qiSchema qiName configDbMaxRows
|
ReqBuilder.readRequest qiSchema qiName configDbMaxRows
|
||||||
(dbRelationships dbStructure)
|
(dbRelationships dbStructure)
|
||||||
@@ -570,32 +562,16 @@ readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure ap
|
|||||||
|
|
||||||
contentTypeHeaders :: RequestContext -> [HTTP.Header]
|
contentTypeHeaders :: RequestContext -> [HTTP.Header]
|
||||||
contentTypeHeaders RequestContext{..} =
|
contentTypeHeaders RequestContext{..} =
|
||||||
ContentType.toHeader ctxContentType : maybeToList (profileHeader ctxApiRequest)
|
ContentType.toHeader (iAcceptContentType ctxApiRequest) : maybeToList (profileHeader ctxApiRequest)
|
||||||
|
|
||||||
requestContentTypes :: AppConfig -> ApiRequest -> [ContentType]
|
-- | If raw(binary) output is requested, check that ContentType is one of the
|
||||||
requestContentTypes conf ApiRequest{..} =
|
-- admitted rawContentTypes and that`?select=...` contains only one field other
|
||||||
case iAction of
|
-- than `*`
|
||||||
ActionRead _ -> defaultContentTypes ++ rawContentTypes conf
|
|
||||||
ActionInvoke _ -> invokeContentTypes
|
|
||||||
ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
|
|
||||||
ActionInfo -> [CTTextCSV]
|
|
||||||
_ -> defaultContentTypes
|
|
||||||
where
|
|
||||||
invokeContentTypes =
|
|
||||||
defaultContentTypes
|
|
||||||
++ rawContentTypes conf
|
|
||||||
++ [CTOpenAPI | ApiRequest.tpIsRootSpec iTarget]
|
|
||||||
defaultContentTypes =
|
|
||||||
[CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
|
|
||||||
-- |
|
|
||||||
-- If raw(binary) output is requested, check that ContentType is one of the admitted
|
|
||||||
-- rawContentTypes and that`?select=...` contains only one field other than `*`
|
|
||||||
binaryField :: Monad m => RequestContext -> ReadRequest -> Handler m (Maybe FieldName)
|
binaryField :: Monad m => RequestContext -> ReadRequest -> Handler m (Maybe FieldName)
|
||||||
binaryField RequestContext{..} readReq
|
binaryField RequestContext{..} readReq
|
||||||
| returnsScalar (iTarget ctxApiRequest) && ctxContentType `elem` rawContentTypes ctxConfig =
|
| returnsScalar (iTarget ctxApiRequest) && iAcceptContentType ctxApiRequest `elem` rawContentTypes ctxConfig =
|
||||||
return $ Just "pgrst_scalar"
|
return $ Just "pgrst_scalar"
|
||||||
| ctxContentType `elem` rawContentTypes ctxConfig =
|
| iAcceptContentType ctxApiRequest `elem` rawContentTypes ctxConfig =
|
||||||
let
|
let
|
||||||
fldNames = fstFieldNames readReq
|
fldNames = fstFieldNames readReq
|
||||||
fieldName = headMay fldNames
|
fieldName = headMay fldNames
|
||||||
@@ -603,7 +579,7 @@ binaryField RequestContext{..} readReq
|
|||||||
if length fldNames == 1 && fieldName /= Just "*" then
|
if length fldNames == 1 && fieldName /= Just "*" then
|
||||||
return fieldName
|
return fieldName
|
||||||
else
|
else
|
||||||
throwError $ Error.BinaryFieldError ctxContentType
|
throwError $ Error.BinaryFieldError (iAcceptContentType ctxApiRequest)
|
||||||
| otherwise =
|
| otherwise =
|
||||||
return Nothing
|
return Nothing
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ data ApiRequestError
|
|||||||
| AmbiguousRelBetween Text Text [Relationship]
|
| AmbiguousRelBetween Text Text [Relationship]
|
||||||
| InvalidFilters
|
| InvalidFilters
|
||||||
| UnacceptableSchema [Text]
|
| UnacceptableSchema [Text]
|
||||||
|
| ContentTypeError [ByteString]
|
||||||
| UnsupportedVerb -- Unreachable?
|
| UnsupportedVerb -- Unreachable?
|
||||||
|
|
||||||
instance PgrstError ApiRequestError where
|
instance PgrstError ApiRequestError where
|
||||||
@@ -71,6 +72,7 @@ instance PgrstError ApiRequestError where
|
|||||||
status (NoRelBetween _ _) = HT.status400
|
status (NoRelBetween _ _) = HT.status400
|
||||||
status AmbiguousRelBetween{} = HT.status300
|
status AmbiguousRelBetween{} = HT.status300
|
||||||
status (UnacceptableSchema _) = HT.status406
|
status (UnacceptableSchema _) = HT.status406
|
||||||
|
status (ContentTypeError _) = HT.status415
|
||||||
|
|
||||||
headers _ = [ContentType.toHeader CTApplicationJSON]
|
headers _ = [ContentType.toHeader CTApplicationJSON]
|
||||||
|
|
||||||
@@ -95,6 +97,8 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
|
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
|
||||||
toJSON (UnacceptableSchema schemas) = JSON.object [
|
toJSON (UnacceptableSchema schemas) = JSON.object [
|
||||||
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
|
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
|
||||||
|
toJSON (ContentTypeError cts) = JSON.object [
|
||||||
|
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
|
||||||
|
|
||||||
compressedRel :: Relationship -> JSON.Value
|
compressedRel :: Relationship -> JSON.Value
|
||||||
compressedRel Relationship{..} =
|
compressedRel Relationship{..} =
|
||||||
@@ -237,7 +241,6 @@ data Error
|
|||||||
| JwtTokenMissing
|
| JwtTokenMissing
|
||||||
| JwtTokenInvalid Text
|
| JwtTokenInvalid Text
|
||||||
| SingularityError Integer
|
| SingularityError Integer
|
||||||
| ContentTypeError [ByteString]
|
|
||||||
| NotFound
|
| NotFound
|
||||||
| ApiRequestError ApiRequestError
|
| ApiRequestError ApiRequestError
|
||||||
| PgErr PgError
|
| PgErr PgError
|
||||||
@@ -252,7 +255,6 @@ instance PgrstError Error where
|
|||||||
status JwtTokenMissing = HT.status500
|
status JwtTokenMissing = HT.status500
|
||||||
status (JwtTokenInvalid _) = HT.unauthorized401
|
status (JwtTokenInvalid _) = HT.unauthorized401
|
||||||
status (SingularityError _) = HT.status406
|
status (SingularityError _) = HT.status406
|
||||||
status (ContentTypeError _) = HT.status415
|
|
||||||
status NotFound = HT.status404
|
status NotFound = HT.status404
|
||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
status (ApiRequestError err) = status err
|
status (ApiRequestError err) = status err
|
||||||
@@ -278,8 +280,6 @@ instance JSON.ToJSON Error where
|
|||||||
toJSON PutMatchingPkError = JSON.object [
|
toJSON PutMatchingPkError = JSON.object [
|
||||||
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
|
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
|
||||||
|
|
||||||
toJSON (ContentTypeError cts) = JSON.object [
|
|
||||||
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
|
|
||||||
toJSON (SingularityError n) = JSON.object [
|
toJSON (SingularityError n) = JSON.object [
|
||||||
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
||||||
"details" .= T.unwords ["Results contain", show n, "rows,", toS (ContentType.toMime CTSingularJSON), "requires 1 row"]]
|
"details" .= T.unwords ["Results contain", show n, "rows,", toS (ContentType.toMime CTSingularJSON), "requires 1 row"]]
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
Module : PostgREST.Request.ApiRequest
|
Module : PostgREST.Request.ApiRequest
|
||||||
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module PostgREST.Request.ApiRequest
|
module PostgREST.Request.ApiRequest
|
||||||
( ApiRequest(..)
|
( ApiRequest(..)
|
||||||
@@ -13,7 +14,6 @@ module PostgREST.Request.ApiRequest
|
|||||||
, Action(..)
|
, Action(..)
|
||||||
, Target(..)
|
, Target(..)
|
||||||
, PayloadJSON(..)
|
, PayloadJSON(..)
|
||||||
, mutuallyAgreeable
|
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ import qualified Data.Vector as V
|
|||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||||
import Data.List (last, lookup, partition)
|
import Data.List (last, lookup, partition, union)
|
||||||
import Data.List.NonEmpty (head)
|
import Data.List.NonEmpty (head)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Ranged.Boundaries (Boundary (..))
|
import Data.Ranged.Boundaries (Boundary (..))
|
||||||
@@ -44,6 +44,7 @@ import Network.Wai (Request (..))
|
|||||||
import Network.Wai.Parse (parseHttpAccept)
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
import Web.Cookie (parseCookiesText)
|
import Web.Cookie (parseCookiesText)
|
||||||
|
|
||||||
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.ContentType (ContentType (..))
|
import PostgREST.ContentType (ContentType (..))
|
||||||
import PostgREST.DbStructure (DbStructure (..))
|
import PostgREST.DbStructure (DbStructure (..))
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
@@ -137,7 +138,6 @@ data ApiRequest = ApiRequest {
|
|||||||
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
|
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
|
||||||
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
||||||
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
||||||
, iAccepts :: [ContentType] -- ^ Content types the client will accept, [CTAny] if no Accept header
|
|
||||||
, iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions
|
, iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions
|
||||||
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
||||||
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
||||||
@@ -158,22 +158,24 @@ data ApiRequest = ApiRequest {
|
|||||||
, iMethod :: ByteString -- ^ Raw request method
|
, iMethod :: ByteString -- ^ Raw request method
|
||||||
, iProfile :: Maybe Schema -- ^ The request profile for enabling use of multiple schemas. Follows the spec in hhttps://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/.
|
, iProfile :: Maybe Schema -- ^ The request profile for enabling use of multiple schemas. Follows the spec in hhttps://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/.
|
||||||
, iSchema :: Schema -- ^ The request schema. Can vary depending on iProfile.
|
, iSchema :: Schema -- ^ The request schema. Can vary depending on iProfile.
|
||||||
|
, iAcceptContentType :: ContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
userApiRequest :: NonEmpty Schema -> Maybe Text -> DbStructure -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
userApiRequest :: AppConfig -> DbStructure -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||||
userApiRequest confSchemas rootSpec dbStructure req reqBody
|
userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
||||||
| isJust profile && fromJust profile `notElem` confSchemas = Left $ UnacceptableSchema $ toList confSchemas
|
| isJust profile && fromJust profile `notElem` configDbSchemas = Left $ UnacceptableSchema $ toList configDbSchemas
|
||||||
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
||||||
| topLevelRange == emptyRange = Left InvalidRange
|
| topLevelRange == emptyRange = Left InvalidRange
|
||||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
||||||
| isLeft parsedColumns = either Left witness parsedColumns
|
| isLeft parsedColumns = either Left witness parsedColumns
|
||||||
| otherwise = Right ApiRequest {
|
| otherwise = do
|
||||||
|
acceptContentType <- findAcceptContentType conf action target accepts
|
||||||
|
return ApiRequest {
|
||||||
iAction = action
|
iAction = action
|
||||||
, iTarget = target
|
, iTarget = target
|
||||||
, iRange = ranges
|
, iRange = ranges
|
||||||
, iTopLevelRange = topLevelRange
|
, iTopLevelRange = topLevelRange
|
||||||
, iAccepts = maybe [CTAny] (map ContentType.decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
|
||||||
, iPayload = relevantPayload
|
, iPayload = relevantPayload
|
||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
|
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
|
||||||
@@ -206,8 +208,10 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
|
|||||||
, iMethod = method
|
, iMethod = method
|
||||||
, iProfile = profile
|
, iProfile = profile
|
||||||
, iSchema = schema
|
, iSchema = schema
|
||||||
|
, iAcceptContentType = acceptContentType
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
|
accepts = maybe [CTAny] (map ContentType.decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
||||||
-- queryString with '+' converted to ' '(space)
|
-- queryString with '+' converted to ' '(space)
|
||||||
qString = parseQueryReplacePlus True $ rawQueryString req
|
qString = parseQueryReplacePlus True $ rawQueryString req
|
||||||
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
||||||
@@ -287,9 +291,9 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
|
|||||||
"OPTIONS" -> ActionInfo
|
"OPTIONS" -> ActionInfo
|
||||||
_ -> ActionInspect{isHead=False}
|
_ -> ActionInspect{isHead=False}
|
||||||
|
|
||||||
defaultSchema = head confSchemas
|
defaultSchema = head configDbSchemas
|
||||||
profile
|
profile
|
||||||
| length confSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
| length configDbSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
||||||
= Nothing
|
= Nothing
|
||||||
| otherwise = case action of
|
| otherwise = case action of
|
||||||
-- POST/PATCH/PUT/DELETE don't use the same header as per the spec
|
-- POST/PATCH/PUT/DELETE don't use the same header as per the spec
|
||||||
@@ -308,7 +312,7 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
|
|||||||
callFindProc proc = findProc (QualifiedIdentifier schema proc) payloadColumns (hasPrefer (show SingleObject)) $ dbProcs dbStructure
|
callFindProc proc = findProc (QualifiedIdentifier schema proc) payloadColumns (hasPrefer (show SingleObject)) $ dbProcs dbStructure
|
||||||
in
|
in
|
||||||
case path of
|
case path of
|
||||||
[] -> case rootSpec of
|
[] -> case configDbRootSpec of
|
||||||
Just pName -> TargetProc (callFindProc pName) True
|
Just pName -> TargetProc (callFindProc pName) True
|
||||||
Nothing -> TargetDefaultSpec schema
|
Nothing -> TargetDefaultSpec schema
|
||||||
[table] -> TargetIdent $ QualifiedIdentifier schema table
|
[table] -> TargetIdent $ QualifiedIdentifier schema table
|
||||||
@@ -423,3 +427,31 @@ payloadAttributes raw json =
|
|||||||
_ -> Just emptyPJArray
|
_ -> Just emptyPJArray
|
||||||
where
|
where
|
||||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
||||||
|
|
||||||
|
findAcceptContentType :: AppConfig -> Action -> Target -> [ContentType] -> Either ApiRequestError ContentType
|
||||||
|
findAcceptContentType conf action target accepts =
|
||||||
|
case mutuallyAgreeable (requestContentTypes conf action target) accepts of
|
||||||
|
Just ct ->
|
||||||
|
Right ct
|
||||||
|
Nothing ->
|
||||||
|
Left . ContentTypeError $ map ContentType.toMime accepts
|
||||||
|
|
||||||
|
requestContentTypes :: AppConfig -> Action -> Target -> [ContentType]
|
||||||
|
requestContentTypes conf action target =
|
||||||
|
case action of
|
||||||
|
ActionRead _ -> defaultContentTypes ++ rawContentTypes conf
|
||||||
|
ActionInvoke _ -> invokeContentTypes
|
||||||
|
ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
|
||||||
|
ActionInfo -> [CTTextCSV]
|
||||||
|
_ -> defaultContentTypes
|
||||||
|
where
|
||||||
|
invokeContentTypes =
|
||||||
|
defaultContentTypes
|
||||||
|
++ rawContentTypes conf
|
||||||
|
++ [CTOpenAPI | tpIsRootSpec target]
|
||||||
|
defaultContentTypes =
|
||||||
|
[CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
|
|
||||||
|
rawContentTypes :: AppConfig -> [ContentType]
|
||||||
|
rawContentTypes AppConfig{..} =
|
||||||
|
(ContentType.decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain]
|
||||||
|
|||||||
Reference in New Issue
Block a user