refactor: rename ContentType to MediaType
The core type was wrongly named as the header
This commit is contained in:
committed by
Steve Chavez
parent
3e83bef9c4
commit
7a7ceaf39a
+1
-1
@@ -44,7 +44,6 @@ library
|
|||||||
PostgREST.Config.JSPath
|
PostgREST.Config.JSPath
|
||||||
PostgREST.Config.PgVersion
|
PostgREST.Config.PgVersion
|
||||||
PostgREST.Config.Proxy
|
PostgREST.Config.Proxy
|
||||||
PostgREST.ContentType
|
|
||||||
PostgREST.Cors
|
PostgREST.Cors
|
||||||
PostgREST.DbStructure
|
PostgREST.DbStructure
|
||||||
PostgREST.DbStructure.Identifiers
|
PostgREST.DbStructure.Identifiers
|
||||||
@@ -55,6 +54,7 @@ library
|
|||||||
PostgREST.GucHeader
|
PostgREST.GucHeader
|
||||||
PostgREST.Logger
|
PostgREST.Logger
|
||||||
PostgREST.Middleware
|
PostgREST.Middleware
|
||||||
|
PostgREST.MediaType
|
||||||
PostgREST.OpenAPI
|
PostgREST.OpenAPI
|
||||||
PostgREST.Query.QueryBuilder
|
PostgREST.Query.QueryBuilder
|
||||||
PostgREST.Query.SqlFragment
|
PostgREST.Query.SqlFragment
|
||||||
|
|||||||
+31
-31
@@ -61,7 +61,6 @@ import PostgREST.Config (AppConfig (..),
|
|||||||
LogLevel (..),
|
LogLevel (..),
|
||||||
OpenAPIMode (..))
|
OpenAPIMode (..))
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
import PostgREST.ContentType (ContentType (..))
|
|
||||||
import PostgREST.DbStructure (DbStructure (..))
|
import PostgREST.DbStructure (DbStructure (..))
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
@@ -73,6 +72,7 @@ import PostgREST.Error (Error)
|
|||||||
import PostgREST.GucHeader (GucHeader,
|
import PostgREST.GucHeader (GucHeader,
|
||||||
addHeadersIfNotIncluded,
|
addHeadersIfNotIncluded,
|
||||||
unwrapGucHeader)
|
unwrapGucHeader)
|
||||||
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.Request.ApiRequest (Action (..),
|
import PostgREST.Request.ApiRequest (Action (..),
|
||||||
ApiRequest (..),
|
ApiRequest (..),
|
||||||
InvokeMethod (..),
|
InvokeMethod (..),
|
||||||
@@ -86,8 +86,8 @@ import PostgREST.Request.ReadQuery (ReadRequest, fstFieldNames)
|
|||||||
import PostgREST.Version (prettyVersion)
|
import PostgREST.Version (prettyVersion)
|
||||||
import PostgREST.Workers (connectionWorker, listener)
|
import PostgREST.Workers (connectionWorker, listener)
|
||||||
|
|
||||||
import qualified PostgREST.ContentType as ContentType
|
|
||||||
import qualified PostgREST.DbStructure.Proc as Proc
|
import qualified PostgREST.DbStructure.Proc as Proc
|
||||||
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import Protolude hiding (Handler)
|
import Protolude hiding (Handler)
|
||||||
|
|
||||||
@@ -267,11 +267,11 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
else
|
else
|
||||||
countQuery
|
countQuery
|
||||||
)
|
)
|
||||||
(iAcceptContentType == CTSingularJSON)
|
(iAcceptMediaType == MTSingularJSON)
|
||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
(iAcceptContentType == CTTextCSV)
|
(iAcceptMediaType == MTTextCSV)
|
||||||
(iAcceptContentType == CTTextXML)
|
(iAcceptMediaType == MTTextXML)
|
||||||
(iAcceptContentType == CTGeoJSON)
|
(iAcceptMediaType == MTGeoJSON)
|
||||||
bField
|
bField
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
]
|
]
|
||||||
++ contentTypeHeaders context
|
++ contentTypeHeaders context
|
||||||
|
|
||||||
failNotSingular iAcceptContentType queryTotal . response status headers $
|
failNotSingular iAcceptMediaType queryTotal . response status headers $
|
||||||
if headersOnly then mempty else LBS.fromStrict body
|
if headersOnly then mempty else LBS.fromStrict body
|
||||||
|
|
||||||
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
|
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
|
||||||
@@ -341,7 +341,7 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
|||||||
toAppliedHeader <$> iPreferResolution
|
toAppliedHeader <$> iPreferResolution
|
||||||
]
|
]
|
||||||
|
|
||||||
failNotSingular iAcceptContentType resQueryTotal $
|
failNotSingular iAcceptMediaType resQueryTotal $
|
||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status201 (headers ++ contentTypeHeaders context) (LBS.fromStrict resBody)
|
response HTTP.status201 (headers ++ contentTypeHeaders context) (LBS.fromStrict resBody)
|
||||||
else
|
else
|
||||||
@@ -368,7 +368,7 @@ handleUpdate identifier context@RequestContext{..} = do
|
|||||||
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
||||||
|
|
||||||
failChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resQueryTotal =<<
|
failChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resQueryTotal =<<
|
||||||
failNotSingular iAcceptContentType resQueryTotal (
|
failNotSingular iAcceptMediaType resQueryTotal (
|
||||||
if fullRepr then
|
if fullRepr then
|
||||||
response status (contentTypeHeaders context ++ [contentRangeHeader]) (LBS.fromStrict resBody)
|
response status (contentTypeHeaders context ++ [contentRangeHeader]) (LBS.fromStrict resBody)
|
||||||
else
|
else
|
||||||
@@ -408,7 +408,7 @@ handleDelete identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
if shouldCount iPreferCount then Just resQueryTotal else Nothing
|
||||||
|
|
||||||
failChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resQueryTotal =<<
|
failChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resQueryTotal =<<
|
||||||
failNotSingular iAcceptContentType resQueryTotal (
|
failNotSingular iAcceptMediaType resQueryTotal (
|
||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200
|
response HTTP.status200
|
||||||
(contentTypeHeaders context ++ [contentRangeHeader])
|
(contentTypeHeaders context ++ [contentRangeHeader])
|
||||||
@@ -462,10 +462,10 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(QueryBuilder.readRequestToQuery req)
|
(QueryBuilder.readRequestToQuery req)
|
||||||
(QueryBuilder.readRequestToCountQuery req)
|
(QueryBuilder.readRequestToCountQuery req)
|
||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
(iAcceptContentType == CTSingularJSON)
|
(iAcceptMediaType == MTSingularJSON)
|
||||||
(iAcceptContentType == CTTextCSV)
|
(iAcceptMediaType == MTTextCSV)
|
||||||
(iAcceptContentType == CTTextXML)
|
(iAcceptMediaType == MTTextXML)
|
||||||
(iAcceptContentType == CTGeoJSON)
|
(iAcceptMediaType == MTGeoJSON)
|
||||||
(iPreferParameters == Just MultipleObjects)
|
(iPreferParameters == Just MultipleObjects)
|
||||||
bField
|
bField
|
||||||
(configDbPreparedStatements ctxConfig)
|
(configDbPreparedStatements ctxConfig)
|
||||||
@@ -476,7 +476,7 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(status, contentRange) =
|
(status, contentRange) =
|
||||||
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
|
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
|
||||||
|
|
||||||
failNotSingular iAcceptContentType queryTotal $
|
failNotSingular iAcceptMediaType queryTotal $
|
||||||
if Proc.procReturnsVoid proc then
|
if Proc.procReturnsVoid proc then
|
||||||
response HTTP.status204 [contentRange] mempty
|
response HTTP.status204 [contentRange] mempty
|
||||||
else
|
else
|
||||||
@@ -503,7 +503,7 @@ handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure
|
|||||||
|
|
||||||
return $
|
return $
|
||||||
Wai.responseLBS HTTP.status200
|
Wai.responseLBS HTTP.status200
|
||||||
(ContentType.toHeader CTOpenAPI : maybeToList (profileHeader apiRequest))
|
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader apiRequest))
|
||||||
(if headersOnly then mempty else body)
|
(if headersOnly then mempty else body)
|
||||||
|
|
||||||
txMode :: ApiRequest -> SQL.Mode
|
txMode :: ApiRequest -> SQL.Mode
|
||||||
@@ -550,10 +550,10 @@ writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@R
|
|||||||
Statements.createWriteStatement
|
Statements.createWriteStatement
|
||||||
(QueryBuilder.readRequestToQuery readReq)
|
(QueryBuilder.readRequestToQuery readReq)
|
||||||
(QueryBuilder.mutateRequestToQuery mutateReq)
|
(QueryBuilder.mutateRequestToQuery mutateReq)
|
||||||
(iAcceptContentType ctxApiRequest == CTSingularJSON)
|
(iAcceptMediaType ctxApiRequest == MTSingularJSON)
|
||||||
isInsert
|
isInsert
|
||||||
(iAcceptContentType ctxApiRequest == CTTextCSV)
|
(iAcceptMediaType ctxApiRequest == MTTextCSV)
|
||||||
(iAcceptContentType ctxApiRequest == CTGeoJSON)
|
(iAcceptMediaType ctxApiRequest == MTGeoJSON)
|
||||||
(iPreferRepresentation ctxApiRequest)
|
(iPreferRepresentation ctxApiRequest)
|
||||||
pkCols
|
pkCols
|
||||||
(configDbPreparedStatements ctxConfig)
|
(configDbPreparedStatements ctxConfig)
|
||||||
@@ -575,9 +575,9 @@ gucResponse gucStatus gucHeaders status headers =
|
|||||||
-- |
|
-- |
|
||||||
-- Fail a response if a single JSON object was requested and not exactly one
|
-- Fail a response if a single JSON object was requested and not exactly one
|
||||||
-- was found.
|
-- was found.
|
||||||
failNotSingular :: ContentType -> Int64 -> Wai.Response -> DbHandler Wai.Response
|
failNotSingular :: MediaType -> Int64 -> Wai.Response -> DbHandler Wai.Response
|
||||||
failNotSingular contentType queryTotal response =
|
failNotSingular mediaType queryTotal response =
|
||||||
if contentType == CTSingularJSON && queryTotal /= 1 then
|
if mediaType == MTSingularJSON && queryTotal /= 1 then
|
||||||
do
|
do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.singularityError queryTotal
|
throwError $ Error.singularityError queryTotal
|
||||||
@@ -611,16 +611,16 @@ readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure ap
|
|||||||
|
|
||||||
contentTypeHeaders :: RequestContext -> [HTTP.Header]
|
contentTypeHeaders :: RequestContext -> [HTTP.Header]
|
||||||
contentTypeHeaders RequestContext{..} =
|
contentTypeHeaders RequestContext{..} =
|
||||||
ContentType.toHeader (iAcceptContentType ctxApiRequest) : maybeToList (profileHeader ctxApiRequest)
|
MediaType.toContentType (iAcceptMediaType ctxApiRequest) : maybeToList (profileHeader ctxApiRequest)
|
||||||
|
|
||||||
-- | If raw(binary) output is requested, check that ContentType is one of the
|
-- | If raw(binary) output is requested, check that MediaType is one of the
|
||||||
-- admitted rawContentTypes and that`?select=...` contains only one field other
|
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
||||||
-- than `*`
|
-- 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) && iAcceptContentType ctxApiRequest `elem` rawContentTypes ctxConfig =
|
| returnsScalar (iTarget ctxApiRequest) && iAcceptMediaType ctxApiRequest `elem` rawMediaTypes ctxConfig =
|
||||||
return $ Just "pgrst_scalar"
|
return $ Just "pgrst_scalar"
|
||||||
| iAcceptContentType ctxApiRequest `elem` rawContentTypes ctxConfig =
|
| iAcceptMediaType ctxApiRequest `elem` rawMediaTypes ctxConfig =
|
||||||
let
|
let
|
||||||
fldNames = fstFieldNames readReq
|
fldNames = fstFieldNames readReq
|
||||||
fieldName = headMay fldNames
|
fieldName = headMay fldNames
|
||||||
@@ -628,13 +628,13 @@ 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 (iAcceptContentType ctxApiRequest)
|
throwError $ Error.BinaryFieldError (iAcceptMediaType ctxApiRequest)
|
||||||
| otherwise =
|
| otherwise =
|
||||||
return Nothing
|
return Nothing
|
||||||
|
|
||||||
rawContentTypes :: AppConfig -> [ContentType]
|
rawMediaTypes :: AppConfig -> [MediaType]
|
||||||
rawContentTypes AppConfig{..} =
|
rawMediaTypes AppConfig{..} =
|
||||||
(ContentType.decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain, CTTextXML]
|
(MediaType.decodeMediaType <$> configRawMediaTypes) `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
||||||
|
|
||||||
profileHeader :: ApiRequest -> Maybe HTTP.Header
|
profileHeader :: ApiRequest -> Maybe HTTP.Header
|
||||||
profileHeader ApiRequest{..} =
|
profileHeader ApiRequest{..} =
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
|
||||||
|
|
||||||
module PostgREST.ContentType
|
|
||||||
( ContentType(..)
|
|
||||||
, toHeader
|
|
||||||
, toMime
|
|
||||||
, decodeContentType
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Internal as BS (c2w)
|
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, hContentType)
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
-- | Enumeration of currently supported response content types
|
|
||||||
data ContentType
|
|
||||||
= CTApplicationJSON
|
|
||||||
| CTSingularJSON
|
|
||||||
| CTGeoJSON
|
|
||||||
| CTTextCSV
|
|
||||||
| CTTextPlain
|
|
||||||
| CTTextXML
|
|
||||||
| CTOpenAPI
|
|
||||||
| CTUrlEncoded
|
|
||||||
| CTOctetStream
|
|
||||||
| CTAny
|
|
||||||
| CTOther ByteString
|
|
||||||
deriving (Eq)
|
|
||||||
|
|
||||||
-- | Convert from ContentType to a full HTTP Header
|
|
||||||
toHeader :: ContentType -> Header
|
|
||||||
toHeader ct = (hContentType, toMime ct <> charset)
|
|
||||||
where
|
|
||||||
charset = case ct of
|
|
||||||
CTOctetStream -> mempty
|
|
||||||
CTOther _ -> mempty
|
|
||||||
_ -> "; charset=utf-8"
|
|
||||||
|
|
||||||
-- | Convert from ContentType to a ByteString representing the mime type
|
|
||||||
toMime :: ContentType -> ByteString
|
|
||||||
toMime CTApplicationJSON = "application/json"
|
|
||||||
toMime CTGeoJSON = "application/geo+json"
|
|
||||||
toMime CTTextCSV = "text/csv"
|
|
||||||
toMime CTTextPlain = "text/plain"
|
|
||||||
toMime CTTextXML = "text/xml"
|
|
||||||
toMime CTOpenAPI = "application/openapi+json"
|
|
||||||
toMime CTSingularJSON = "application/vnd.pgrst.object+json"
|
|
||||||
toMime CTUrlEncoded = "application/x-www-form-urlencoded"
|
|
||||||
toMime CTOctetStream = "application/octet-stream"
|
|
||||||
toMime CTAny = "*/*"
|
|
||||||
toMime (CTOther ct) = ct
|
|
||||||
|
|
||||||
-- | Convert from ByteString to ContentType. Warning: discards MIME parameters
|
|
||||||
decodeContentType :: BS.ByteString -> ContentType
|
|
||||||
decodeContentType ct =
|
|
||||||
case BS.takeWhile (/= BS.c2w ';') ct of
|
|
||||||
"application/json" -> CTApplicationJSON
|
|
||||||
"application/geo+json" -> CTGeoJSON
|
|
||||||
"text/csv" -> CTTextCSV
|
|
||||||
"text/plain" -> CTTextPlain
|
|
||||||
"text/xml" -> CTTextXML
|
|
||||||
"application/openapi+json" -> CTOpenAPI
|
|
||||||
"application/vnd.pgrst.object+json" -> CTSingularJSON
|
|
||||||
"application/vnd.pgrst.object" -> CTSingularJSON
|
|
||||||
"application/x-www-form-urlencoded" -> CTUrlEncoded
|
|
||||||
"application/octet-stream" -> CTOctetStream
|
|
||||||
"*/*" -> CTAny
|
|
||||||
ct' -> CTOther ct'
|
|
||||||
+19
-19
@@ -29,8 +29,8 @@ import Network.Wai (Response, responseLBS)
|
|||||||
|
|
||||||
import Network.HTTP.Types.Header (Header)
|
import Network.HTTP.Types.Header (Header)
|
||||||
|
|
||||||
import PostgREST.ContentType (ContentType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import qualified PostgREST.ContentType as ContentType
|
import qualified PostgREST.MediaType as MediaType
|
||||||
import PostgREST.Request.Types (ApiRequestError (..),
|
import PostgREST.Request.Types (ApiRequestError (..),
|
||||||
QPError (..))
|
QPError (..))
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ instance PgrstError ApiRequestError where
|
|||||||
status ActionInappropriate = HTTP.status405
|
status ActionInappropriate = HTTP.status405
|
||||||
status AmbiguousRelBetween{} = HTTP.status300
|
status AmbiguousRelBetween{} = HTTP.status300
|
||||||
status AmbiguousRpc{} = HTTP.status300
|
status AmbiguousRpc{} = HTTP.status300
|
||||||
status ContentTypeError{} = HTTP.status415
|
status MediaTypeError{} = HTTP.status415
|
||||||
status InvalidBody{} = HTTP.status400
|
status InvalidBody{} = HTTP.status400
|
||||||
status InvalidFilters = HTTP.status405
|
status InvalidFilters = HTTP.status405
|
||||||
status InvalidRange = HTTP.status416
|
status InvalidRange = HTTP.status416
|
||||||
@@ -70,7 +70,7 @@ instance PgrstError ApiRequestError where
|
|||||||
status UnacceptableSchema{} = HTTP.status406
|
status UnacceptableSchema{} = HTTP.status406
|
||||||
status LimitNoOrderError = HTTP.status400
|
status LimitNoOrderError = HTTP.status400
|
||||||
|
|
||||||
headers _ = [ContentType.toHeader CTApplicationJSON]
|
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON ApiRequestError where
|
instance JSON.ToJSON ApiRequestError where
|
||||||
toJSON (QueryParamError (QPError message details)) = JSON.object [
|
toJSON (QueryParamError (QPError message details)) = JSON.object [
|
||||||
@@ -108,9 +108,9 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas),
|
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
toJSON (ContentTypeError cts) = JSON.object [
|
toJSON (MediaTypeError cts) = JSON.object [
|
||||||
"code" .= ApiRequestErrorCode07,
|
"code" .= ApiRequestErrorCode07,
|
||||||
"message" .= ("None of these Content-Types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)),
|
"message" .= ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
toJSON (NotEmbedded resource) = JSON.object [
|
toJSON (NotEmbedded resource) = JSON.object [
|
||||||
@@ -147,10 +147,10 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
||||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||||
(True, _, _) -> " function with a single json or jsonb parameter"
|
(True, _, _) -> " function with a single json or jsonb parameter"
|
||||||
(_, True, CTTextPlain) -> " function with a single unnamed text parameter"
|
(_, True, MTTextPlain) -> " function with a single unnamed text parameter"
|
||||||
(_, True, CTTextXML) -> " function with a single unnamed xml parameter"
|
(_, True, MTTextXML) -> " function with a single unnamed xml parameter"
|
||||||
(_, True, CTOctetStream) -> " function with a single unnamed bytea parameter"
|
(_, True, MTOctetStream) -> " function with a single unnamed bytea parameter"
|
||||||
(_, True, CTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
|
(_, True, MTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
|
||||||
_ -> prms <> " function") <>
|
_ -> prms <> " function") <>
|
||||||
" in the schema cache"),
|
" in the schema cache"),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
@@ -200,8 +200,8 @@ instance PgrstError PgError where
|
|||||||
|
|
||||||
headers err =
|
headers err =
|
||||||
if status err == HTTP.status401
|
if status err == HTTP.status401
|
||||||
then [ContentType.toHeader CTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
then [MediaType.toContentType MTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
||||||
else [ContentType.toHeader CTApplicationJSON]
|
else [MediaType.toContentType MTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON PgError where
|
instance JSON.ToJSON PgError where
|
||||||
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||||
@@ -304,7 +304,7 @@ checkIsFatal _ = Nothing
|
|||||||
|
|
||||||
data Error
|
data Error
|
||||||
= ApiRequestError ApiRequestError
|
= ApiRequestError ApiRequestError
|
||||||
| BinaryFieldError ContentType
|
| BinaryFieldError MediaType
|
||||||
| GucHeadersError
|
| GucHeadersError
|
||||||
| GucStatusError
|
| GucStatusError
|
||||||
| JwtTokenInvalid Text
|
| JwtTokenInvalid Text
|
||||||
@@ -335,11 +335,11 @@ instance PgrstError Error where
|
|||||||
status UnsupportedVerb{} = HTTP.status405
|
status UnsupportedVerb{} = HTTP.status405
|
||||||
|
|
||||||
headers (ApiRequestError err) = headers err
|
headers (ApiRequestError err) = headers err
|
||||||
headers (JwtTokenInvalid m) = [ContentType.toHeader CTApplicationJSON, invalidTokenHeader m]
|
headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m]
|
||||||
headers JwtTokenRequired = [ContentType.toHeader CTApplicationJSON, requiredTokenHeader]
|
headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader]
|
||||||
headers (PgErr err) = headers err
|
headers (PgErr err) = headers err
|
||||||
headers SingularityError{} = [ContentType.toHeader CTSingularJSON]
|
headers SingularityError{} = [MediaType.toContentType MTSingularJSON]
|
||||||
headers _ = [ContentType.toHeader CTApplicationJSON]
|
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON Error where
|
instance JSON.ToJSON Error where
|
||||||
toJSON NoSchemaCacheError = JSON.object [
|
toJSON NoSchemaCacheError = JSON.object [
|
||||||
@@ -382,7 +382,7 @@ instance JSON.ToJSON Error where
|
|||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
toJSON (BinaryFieldError ct) = JSON.object [
|
toJSON (BinaryFieldError ct) = JSON.object [
|
||||||
"code" .= ApiRequestErrorCode13,
|
"code" .= ApiRequestErrorCode13,
|
||||||
"message" .= ((T.decodeUtf8 (ContentType.toMime ct) <> " requested but more than one column was selected") :: Text),
|
"message" .= ((T.decodeUtf8 (MediaType.toMime ct) <> " requested but more than one column was selected") :: Text),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ instance JSON.ToJSON Error where
|
|||||||
toJSON (SingularityError n) = JSON.object [
|
toJSON (SingularityError n) = JSON.object [
|
||||||
"code" .= ApiRequestErrorCode16,
|
"code" .= ApiRequestErrorCode16,
|
||||||
"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,", T.decodeUtf8 (ContentType.toMime CTSingularJSON), "requires 1 row"],
|
"details" .= T.unwords ["Results contain", show n, "rows,", T.decodeUtf8 (MediaType.toMime MTSingularJSON), "requires 1 row"],
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
toJSON (UnsupportedVerb verb) = JSON.object [
|
toJSON (UnsupportedVerb verb) = JSON.object [
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
|
||||||
|
module PostgREST.MediaType
|
||||||
|
( MediaType(..)
|
||||||
|
, toContentType
|
||||||
|
, toMime
|
||||||
|
, decodeMediaType
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Internal as BS (c2w)
|
||||||
|
|
||||||
|
import Network.HTTP.Types.Header (Header, hContentType)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
-- | Enumeration of currently supported media types
|
||||||
|
data MediaType
|
||||||
|
= MTApplicationJSON
|
||||||
|
| MTSingularJSON
|
||||||
|
| MTGeoJSON
|
||||||
|
| MTTextCSV
|
||||||
|
| MTTextPlain
|
||||||
|
| MTTextXML
|
||||||
|
| MTOpenAPI
|
||||||
|
| MTUrlEncoded
|
||||||
|
| MTOctetStream
|
||||||
|
| MTAny
|
||||||
|
| MTOther ByteString
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
-- | Convert MediaType to a Content-Type HTTP Header
|
||||||
|
toContentType :: MediaType -> Header
|
||||||
|
toContentType ct = (hContentType, toMime ct <> charset)
|
||||||
|
where
|
||||||
|
charset = case ct of
|
||||||
|
MTOctetStream -> mempty
|
||||||
|
MTOther _ -> mempty
|
||||||
|
_ -> "; charset=utf-8"
|
||||||
|
|
||||||
|
-- | Convert from MediaType to a ByteString representing the mime type
|
||||||
|
toMime :: MediaType -> ByteString
|
||||||
|
toMime MTApplicationJSON = "application/json"
|
||||||
|
toMime MTGeoJSON = "application/geo+json"
|
||||||
|
toMime MTTextCSV = "text/csv"
|
||||||
|
toMime MTTextPlain = "text/plain"
|
||||||
|
toMime MTTextXML = "text/xml"
|
||||||
|
toMime MTOpenAPI = "application/openapi+json"
|
||||||
|
toMime MTSingularJSON = "application/vnd.pgrst.object+json"
|
||||||
|
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
|
||||||
|
toMime MTOctetStream = "application/octet-stream"
|
||||||
|
toMime MTAny = "*/*"
|
||||||
|
toMime (MTOther ct) = ct
|
||||||
|
|
||||||
|
-- | Convert from ByteString to MediaType. Warning: discards MIME parameters
|
||||||
|
decodeMediaType :: BS.ByteString -> MediaType
|
||||||
|
decodeMediaType ct =
|
||||||
|
case BS.takeWhile (/= BS.c2w ';') ct of
|
||||||
|
"application/json" -> MTApplicationJSON
|
||||||
|
"application/geo+json" -> MTGeoJSON
|
||||||
|
"text/csv" -> MTTextCSV
|
||||||
|
"text/plain" -> MTTextPlain
|
||||||
|
"text/xml" -> MTTextXML
|
||||||
|
"application/openapi+json" -> MTOpenAPI
|
||||||
|
"application/vnd.pgrst.object+json" -> MTSingularJSON
|
||||||
|
"application/vnd.pgrst.object" -> MTSingularJSON
|
||||||
|
"application/x-www-form-urlencoded" -> MTUrlEncoded
|
||||||
|
"application/octet-stream" -> MTOctetStream
|
||||||
|
"*/*" -> MTAny
|
||||||
|
ct' -> MTOther ct'
|
||||||
@@ -37,7 +37,7 @@ import PostgREST.DbStructure.Table (Column (..), Table (..),
|
|||||||
TablesMap)
|
TablesMap)
|
||||||
import PostgREST.Version (docsVersion, prettyVersion)
|
import PostgREST.Version (docsVersion, prettyVersion)
|
||||||
|
|
||||||
import PostgREST.ContentType
|
import PostgREST.MediaType
|
||||||
|
|
||||||
import Protolude hiding (Proxy, get)
|
import Protolude hiding (Proxy, get)
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ encode conf dbStructure tables procs schemaDescription =
|
|||||||
(proxyUri conf)
|
(proxyUri conf)
|
||||||
schemaDescription
|
schemaDescription
|
||||||
|
|
||||||
makeMimeList :: [ContentType] -> MimeList
|
makeMimeList :: [MediaType] -> MimeList
|
||||||
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
|
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
|
||||||
|
|
||||||
toSwaggerType :: Text -> Maybe (SwaggerType t)
|
toSwaggerType :: Text -> Maybe (SwaggerType t)
|
||||||
@@ -294,7 +294,7 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
|||||||
& description .~ mfilter (/="") pDesc
|
& description .~ mfilter (/="") pDesc
|
||||||
& parameters .~ makeProcParam pd
|
& parameters .~ makeProcParam pd
|
||||||
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||||
& produces ?~ makeMimeList [CTApplicationJSON, CTSingularJSON]
|
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON]
|
||||||
& at 200 ?~ "OK"
|
& at 200 ?~ "OK"
|
||||||
pe = (mempty :: PathItem) & post ?~ postOp
|
pe = (mempty :: PathItem) & post ?~ postOp
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ makeRootPathItem = ("/", p)
|
|||||||
getOp = (mempty :: Operation)
|
getOp = (mempty :: Operation)
|
||||||
& tags .~ Set.fromList ["Introspection"]
|
& tags .~ Set.fromList ["Introspection"]
|
||||||
& summary ?~ "OpenAPI description (this document)"
|
& summary ?~ "OpenAPI description (this document)"
|
||||||
& produces ?~ makeMimeList [CTOpenAPI, CTApplicationJSON]
|
& produces ?~ makeMimeList [MTOpenAPI, MTApplicationJSON]
|
||||||
& at 200 ?~ "OK"
|
& at 200 ?~ "OK"
|
||||||
pr = (mempty :: PathItem) & get ?~ getOp
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
p = pr
|
p = pr
|
||||||
@@ -336,8 +336,8 @@ postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger)
|
|||||||
& definitions .~ fromList (makeTableDef rels <$> ti)
|
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||||
& parameters .~ fromList (makeParamDefs ti)
|
& parameters .~ fromList (makeParamDefs ti)
|
||||||
& paths .~ makePathItems pds ti
|
& paths .~ makePathItems pds ti
|
||||||
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
|
||||||
& consumes .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
|
||||||
where
|
where
|
||||||
s' = if s == "http" then Http else Https
|
s' = if s == "http" then Http else Https
|
||||||
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
|
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module PostgREST.Request.ApiRequest
|
|||||||
( ApiRequest(..)
|
( ApiRequest(..)
|
||||||
, InvokeMethod(..)
|
, InvokeMethod(..)
|
||||||
, Mutation(..)
|
, Mutation(..)
|
||||||
, ContentType(..)
|
, MediaType(..)
|
||||||
, Action(..)
|
, Action(..)
|
||||||
, Target(..)
|
, Target(..)
|
||||||
, Payload(..)
|
, Payload(..)
|
||||||
@@ -45,13 +45,13 @@ import Web.Cookie (parseCookies)
|
|||||||
|
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
OpenAPIMode (..))
|
OpenAPIMode (..))
|
||||||
import PostgREST.ContentType (ContentType (..))
|
|
||||||
import PostgREST.DbStructure (DbStructure (..))
|
import PostgREST.DbStructure (DbStructure (..))
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
ProcParam (..), ProcsMap)
|
ProcParam (..), ProcsMap)
|
||||||
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
hasLimitZero,
|
hasLimitZero,
|
||||||
limitZeroRange,
|
limitZeroRange,
|
||||||
@@ -64,7 +64,7 @@ import PostgREST.Request.Preferences (PreferCount (..),
|
|||||||
import PostgREST.Request.QueryParams (QueryParams (..))
|
import PostgREST.Request.QueryParams (QueryParams (..))
|
||||||
import PostgREST.Request.Types (ApiRequestError (..))
|
import PostgREST.Request.Types (ApiRequestError (..))
|
||||||
|
|
||||||
import qualified PostgREST.ContentType as ContentType
|
import qualified PostgREST.MediaType as MediaType
|
||||||
import qualified PostgREST.Request.Preferences as Preferences
|
import qualified PostgREST.Request.Preferences as Preferences
|
||||||
import qualified PostgREST.Request.QueryParams as QueryParams
|
import qualified PostgREST.Request.QueryParams as QueryParams
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ 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
|
, iAcceptMediaType :: MediaType
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
@@ -191,7 +191,7 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
||||||
| method == "PUT" && topLevelRange /= allRange = Left PutRangeNotAllowedError
|
| method == "PUT" && topLevelRange /= allRange = Left PutRangeNotAllowedError
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
acceptContentType <- findAcceptContentType conf action path accepts
|
acceptMediaType <- findAcceptMediaType conf action path accepts
|
||||||
checkedTarget <- target
|
checkedTarget <- target
|
||||||
return ApiRequest {
|
return ApiRequest {
|
||||||
iAction = action
|
iAction = action
|
||||||
@@ -212,10 +212,10 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
, iMethod = method
|
, iMethod = method
|
||||||
, iProfile = profile
|
, iProfile = profile
|
||||||
, iSchema = schema
|
, iSchema = schema
|
||||||
, iAcceptContentType = acceptContentType
|
, iAcceptMediaType = acceptMediaType
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
accepts = maybe [CTAny] (map ContentType.decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
accepts = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
|
||||||
|
|
||||||
expectParams = isTargetingProc && method /= "POST"
|
expectParams = isTargetingProc && method /= "POST"
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
isTargetingDefaultSpec = case path of
|
isTargetingDefaultSpec = case path of
|
||||||
PathInfo{pIsDefaultSpec=True} -> True
|
PathInfo{pIsDefaultSpec=True} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
contentType = maybe CTApplicationJSON ContentType.decodeContentType $ lookupHeader "content-type"
|
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
||||||
|
|
||||||
columns = case action of
|
columns = case action of
|
||||||
ActionMutate MutationCreate -> qsColumns
|
ActionMutate MutationCreate -> qsColumns
|
||||||
@@ -234,33 +234,33 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
payloadColumns =
|
payloadColumns =
|
||||||
case (contentType, action) of
|
case (contentMediaType, action) of
|
||||||
(_, ActionInvoke InvGet) -> S.fromList $ fst <$> qsParams
|
(_, ActionInvoke InvGet) -> S.fromList $ fst <$> qsParams
|
||||||
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> qsParams
|
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> qsParams
|
||||||
(CTUrlEncoded, _) -> S.fromList $ map (T.decodeUtf8 . fst) $ parseSimpleQuery $ LBS.toStrict reqBody
|
(MTUrlEncoded, _) -> S.fromList $ map (T.decodeUtf8 . fst) $ parseSimpleQuery $ LBS.toStrict reqBody
|
||||||
_ -> case (relevantPayload, columns) of
|
_ -> case (relevantPayload, columns) of
|
||||||
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||||
(Just RawJSON{}, Just cls) -> cls
|
(Just RawJSON{}, Just cls) -> cls
|
||||||
_ -> S.empty
|
_ -> S.empty
|
||||||
payload :: Either ByteString Payload
|
payload :: Either ByteString Payload
|
||||||
payload = case (contentType, isTargetingProc) of
|
payload = case (contentMediaType, isTargetingProc) of
|
||||||
(CTApplicationJSON, _) ->
|
(MTApplicationJSON, _) ->
|
||||||
if isJust columns
|
if isJust columns
|
||||||
then Right $ RawJSON reqBody
|
then Right $ RawJSON reqBody
|
||||||
else note "All object keys must match" . payloadAttributes reqBody
|
else note "All object keys must match" . payloadAttributes reqBody
|
||||||
=<< if LBS.null reqBody && isTargetingProc
|
=<< if LBS.null reqBody && isTargetingProc
|
||||||
then Right emptyObject
|
then Right emptyObject
|
||||||
else first BS.pack $ JSON.eitherDecode reqBody
|
else first BS.pack $ JSON.eitherDecode reqBody
|
||||||
(CTTextCSV, _) -> do
|
(MTTextCSV, _) -> do
|
||||||
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
||||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||||
(CTUrlEncoded, _) ->
|
(MTUrlEncoded, _) ->
|
||||||
let paramsMap = HM.fromList $ (T.decodeUtf8 *** JSON.String . T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody) in
|
let paramsMap = HM.fromList $ (T.decodeUtf8 *** JSON.String . T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody) in
|
||||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
|
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
|
||||||
(CTTextPlain, True) -> Right $ RawPay reqBody
|
(MTTextPlain, True) -> Right $ RawPay reqBody
|
||||||
(CTTextXML, True) -> Right $ RawPay reqBody
|
(MTTextXML, True) -> Right $ RawPay reqBody
|
||||||
(CTOctetStream, True) -> Right $ RawPay reqBody
|
(MTOctetStream, True) -> Right $ RawPay reqBody
|
||||||
(ct, _) -> Left $ "Content-Type not acceptable: " <> ContentType.toMime ct
|
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
|
||||||
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
action =
|
action =
|
||||||
case method of
|
case method of
|
||||||
@@ -300,7 +300,7 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
let
|
let
|
||||||
callFindProc procSch procNam = findProc
|
callFindProc procSch procNam = findProc
|
||||||
(QualifiedIdentifier procSch procNam) payloadColumns (preferParameters == Just SingleObject) (dbProcs dbStructure)
|
(QualifiedIdentifier procSch procNam) payloadColumns (preferParameters == Just SingleObject) (dbProcs dbStructure)
|
||||||
contentType (action == ActionInvoke InvPost)
|
contentMediaType (action == ActionInvoke InvPost)
|
||||||
in
|
in
|
||||||
case path of
|
case path of
|
||||||
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
||||||
@@ -309,19 +309,19 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
| otherwise -> Right $ TargetIdent $ QualifiedIdentifier pSchema pName
|
| otherwise -> Right $ TargetIdent $ QualifiedIdentifier pSchema pName
|
||||||
PathUnknown -> Right TargetUnknown
|
PathUnknown -> Right TargetUnknown
|
||||||
|
|
||||||
shouldParsePayload = case (action, contentType) of
|
shouldParsePayload = case (action, contentMediaType) of
|
||||||
(ActionMutate MutationCreate, _) -> True
|
(ActionMutate MutationCreate, _) -> True
|
||||||
(ActionInvoke InvPost, CTUrlEncoded) -> False
|
(ActionInvoke InvPost, MTUrlEncoded) -> False
|
||||||
(ActionInvoke InvPost, _) -> True
|
(ActionInvoke InvPost, _) -> True
|
||||||
(ActionMutate MutationSingleUpsert, _) -> True
|
(ActionMutate MutationSingleUpsert, _) -> True
|
||||||
(ActionMutate MutationUpdate, _) -> True
|
(ActionMutate MutationUpdate, _) -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
relevantPayload = case (contentType, action) of
|
relevantPayload = case (contentMediaType, action) of
|
||||||
-- Though ActionInvoke GET/HEAD doesn't really have a payload, we use the payload variable as a way
|
-- Though ActionInvoke GET/HEAD doesn't really have a payload, we use the payload variable as a way
|
||||||
-- to store the query string arguments to the function.
|
-- to store the query string arguments to the function.
|
||||||
(_, ActionInvoke InvGet) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
(_, ActionInvoke InvGet) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
||||||
(_, ActionInvoke InvHead) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
(_, ActionInvoke InvHead) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
||||||
(CTUrlEncoded, ActionInvoke InvPost) -> targetToJsonRpcParams (rightToMaybe target) $ (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
(MTUrlEncoded, ActionInvoke InvPost) -> targetToJsonRpcParams (rightToMaybe target) $ (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
||||||
_ | shouldParsePayload -> rightToMaybe payload
|
_ | shouldParsePayload -> rightToMaybe payload
|
||||||
| otherwise -> Nothing
|
| otherwise -> Nothing
|
||||||
path =
|
path =
|
||||||
@@ -348,15 +348,15 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{..
|
|||||||
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
|
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Find the best match from a list of content types accepted by the
|
Find the best match from a list of media types accepted by the
|
||||||
client in order of decreasing preference and a list of types
|
client in order of decreasing preference and a list of types
|
||||||
producible by the server. If there is no match but the client
|
producible by the server. If there is no match but the client
|
||||||
accepts */* then return the top server pick.
|
accepts */* then return the top server pick.
|
||||||
-}
|
-}
|
||||||
mutuallyAgreeable :: [ContentType] -> [ContentType] -> Maybe ContentType
|
mutuallyAgreeable :: [MediaType] -> [MediaType] -> Maybe MediaType
|
||||||
mutuallyAgreeable sProduces cAccepts =
|
mutuallyAgreeable sProduces cAccepts =
|
||||||
let exact = listToMaybe $ L.intersect cAccepts sProduces in
|
let exact = listToMaybe $ L.intersect cAccepts sProduces in
|
||||||
if isNothing exact && CTAny `elem` cAccepts
|
if isNothing exact && MTAny `elem` cAccepts
|
||||||
then listToMaybe sProduces
|
then listToMaybe sProduces
|
||||||
else exact
|
else exact
|
||||||
|
|
||||||
@@ -409,42 +409,42 @@ payloadAttributes raw json =
|
|||||||
where
|
where
|
||||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
||||||
|
|
||||||
findAcceptContentType :: AppConfig -> Action -> Path -> [ContentType] -> Either ApiRequestError ContentType
|
findAcceptMediaType :: AppConfig -> Action -> Path -> [MediaType] -> Either ApiRequestError MediaType
|
||||||
findAcceptContentType conf action path accepts =
|
findAcceptMediaType conf action path accepts =
|
||||||
case mutuallyAgreeable (requestContentTypes conf action path) accepts of
|
case mutuallyAgreeable (requestMediaTypes conf action path) accepts of
|
||||||
Just ct ->
|
Just ct ->
|
||||||
Right ct
|
Right ct
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Left . ContentTypeError $ map ContentType.toMime accepts
|
Left . MediaTypeError $ map MediaType.toMime accepts
|
||||||
|
|
||||||
requestContentTypes :: AppConfig -> Action -> Path -> [ContentType]
|
requestMediaTypes :: AppConfig -> Action -> Path -> [MediaType]
|
||||||
requestContentTypes conf action path =
|
requestMediaTypes conf action path =
|
||||||
case action of
|
case action of
|
||||||
ActionRead _ -> defaultContentTypes ++ rawContentTypes conf
|
ActionRead _ -> defaultMediaTypes ++ rawMediaTypes conf
|
||||||
ActionInvoke _ -> invokeContentTypes
|
ActionInvoke _ -> invokeMediaTypes
|
||||||
ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
|
ActionInspect _ -> [MTOpenAPI, MTApplicationJSON]
|
||||||
ActionInfo -> [CTTextCSV]
|
ActionInfo -> [MTTextCSV]
|
||||||
_ -> defaultContentTypes
|
_ -> defaultMediaTypes
|
||||||
where
|
where
|
||||||
invokeContentTypes =
|
invokeMediaTypes =
|
||||||
defaultContentTypes
|
defaultMediaTypes
|
||||||
++ rawContentTypes conf
|
++ rawMediaTypes conf
|
||||||
++ [CTOpenAPI | pIsRootSpec path]
|
++ [MTOpenAPI | pIsRootSpec path]
|
||||||
defaultContentTypes =
|
defaultMediaTypes =
|
||||||
[CTApplicationJSON, CTSingularJSON, CTGeoJSON, CTTextCSV]
|
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV]
|
||||||
|
|
||||||
rawContentTypes :: AppConfig -> [ContentType]
|
rawMediaTypes :: AppConfig -> [MediaType]
|
||||||
rawContentTypes AppConfig{..} =
|
rawMediaTypes AppConfig{..} =
|
||||||
(ContentType.decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain, CTTextXML]
|
(MediaType.decodeMediaType <$> configRawMediaTypes) `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||||
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
||||||
-}
|
-}
|
||||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ContentType -> Bool -> Either ApiRequestError ProcDescription
|
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> MediaType -> Bool -> Either ApiRequestError ProcDescription
|
||||||
findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
|
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
||||||
case matchProc of
|
case matchProc of
|
||||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentType isInvPost
|
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost
|
||||||
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||||
([], [proc]) -> Right proc
|
([], [proc]) -> Right proc
|
||||||
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||||
@@ -462,12 +462,12 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
|
|||||||
| otherwise = (ts,fs)
|
| otherwise = (ts,fs)
|
||||||
-- If the function is called with post and has a single unnamed parameter
|
-- If the function is called with post and has a single unnamed parameter
|
||||||
-- it can be called depending on content type and the parameter type
|
-- it can be called depending on content type and the parameter type
|
||||||
hasSingleUnnamedParam ProcDescription{pdParams=[ProcParam{ppType}]} = isInvPost && case (contentType, ppType) of
|
hasSingleUnnamedParam ProcDescription{pdParams=[ProcParam{ppType}]} = isInvPost && case (contentMediaType, ppType) of
|
||||||
(CTApplicationJSON, "json") -> True
|
(MTApplicationJSON, "json") -> True
|
||||||
(CTApplicationJSON, "jsonb") -> True
|
(MTApplicationJSON, "jsonb") -> True
|
||||||
(CTTextPlain, "text") -> True
|
(MTTextPlain, "text") -> True
|
||||||
(CTTextXML, "xml") -> True
|
(MTTextXML, "xml") -> True
|
||||||
(CTOctetStream, "bytea") -> True
|
(MTOctetStream, "bytea") -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
hasSingleUnnamedParam _ = False
|
hasSingleUnnamedParam _ = False
|
||||||
matchesParams proc =
|
matchesParams proc =
|
||||||
@@ -480,7 +480,7 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
|
|||||||
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
|
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
|
||||||
-- If the function has no parameters, the arguments keys must be empty as well
|
-- If the function has no parameters, the arguments keys must be empty as well
|
||||||
else if null params
|
else if null params
|
||||||
then null argumentsKeys && not (isInvPost && contentType `elem` [CTOctetStream, CTTextPlain, CTTextXML])
|
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
|
||||||
-- A function has optional and required parameters. Optional parameters have a default value and
|
-- A function has optional and required parameters. Optional parameters have a default value and
|
||||||
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
||||||
else case L.partition ppReq params of
|
else case L.partition ppReq params of
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ module PostgREST.Request.Types
|
|||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
|
||||||
import PostgREST.ContentType (ContentType (..))
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
ProcParam (..))
|
ProcParam (..))
|
||||||
import PostgREST.DbStructure.Relationship (Relationship)
|
import PostgREST.DbStructure.Relationship (Relationship)
|
||||||
|
import PostgREST.MediaType (MediaType (..))
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -50,13 +50,13 @@ data ApiRequestError
|
|||||||
= ActionInappropriate
|
= ActionInappropriate
|
||||||
| AmbiguousRelBetween Text Text [Relationship]
|
| AmbiguousRelBetween Text Text [Relationship]
|
||||||
| AmbiguousRpc [ProcDescription]
|
| AmbiguousRpc [ProcDescription]
|
||||||
| ContentTypeError [ByteString]
|
| MediaTypeError [ByteString]
|
||||||
| InvalidBody ByteString
|
| InvalidBody ByteString
|
||||||
| InvalidFilters
|
| InvalidFilters
|
||||||
| InvalidRange
|
| InvalidRange
|
||||||
| LimitNoOrderError
|
| LimitNoOrderError
|
||||||
| NoRelBetween Text Text Text
|
| NoRelBetween Text Text Text
|
||||||
| NoRpc Text Text [Text] Bool ContentType Bool
|
| NoRpc Text Text [Text] Bool MediaType Bool
|
||||||
| NotEmbedded Text
|
| NotEmbedded Text
|
||||||
| ParseRequestError Text Text
|
| ParseRequestError Text Text
|
||||||
| PutRangeNotAllowedError
|
| PutRangeNotAllowedError
|
||||||
|
|||||||
@@ -909,7 +909,7 @@ spec actualPgVersion = do
|
|||||||
request methodGet "/simple_pk"
|
request methodGet "/simple_pk"
|
||||||
(acceptHdrs "text/unknowntype") ""
|
(acceptHdrs "text/unknowntype") ""
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"None of these Content-Types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
|
[json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
|
||||||
{ matchStatus = 415
|
{ matchStatus = 415
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user