Accept text/plain and text/html for raw output (#1330)
This commit is contained in:
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #1278, Add db-pool-timeout config option - @qu4tro
|
- #1278, Add db-pool-timeout config option - @qu4tro
|
||||||
- #1285, Abort on wrong database password - @qu4tro
|
- #1285, Abort on wrong database password - @qu4tro
|
||||||
- #790, Allow override of OpenAPI spec through `root-spec` config option - @steve-chavez
|
- #790, Allow override of OpenAPI spec through `root-spec` config option - @steve-chavez
|
||||||
|
- #1308, Accept `text/plain` and `text/html` for raw output - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+10
-28
@@ -14,17 +14,16 @@ module PostgREST.ApiRequest (
|
|||||||
, userApiRequest
|
, userApiRequest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Internal as BS (c2w)
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.Csv as CSV
|
||||||
import qualified Data.Csv as CSV
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.List as L
|
||||||
import qualified Data.List as L
|
import qualified Data.Set as S
|
||||||
import qualified Data.Set as S
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text as T
|
import qualified Data.Vector as V
|
||||||
import qualified Data.Vector as V
|
|
||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||||
@@ -264,23 +263,6 @@ mutuallyAgreeable sProduces cAccepts =
|
|||||||
then listToMaybe sProduces
|
then listToMaybe sProduces
|
||||||
else exact
|
else exact
|
||||||
|
|
||||||
-- PRIVATE ---------------------------------------------------------------
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Warning: discards MIME parameters
|
|
||||||
-}
|
|
||||||
decodeContentType :: BS.ByteString -> ContentType
|
|
||||||
decodeContentType ct =
|
|
||||||
case BS.takeWhile (/= BS.c2w ';') ct of
|
|
||||||
"application/json" -> CTApplicationJSON
|
|
||||||
"text/csv" -> CTTextCSV
|
|
||||||
"application/openapi+json" -> CTOpenAPI
|
|
||||||
"application/vnd.pgrst.object+json" -> CTSingularJSON
|
|
||||||
"application/vnd.pgrst.object" -> CTSingularJSON
|
|
||||||
"application/octet-stream" -> CTOctetStream
|
|
||||||
"*/*" -> CTAny
|
|
||||||
ct' -> CTOther ct'
|
|
||||||
|
|
||||||
type CsvData = V.Vector (M.HashMap Text BL.ByteString)
|
type CsvData = V.Vector (M.HashMap Text BL.ByteString)
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
|
|||||||
+16
-14
@@ -39,7 +39,7 @@ import PostgREST.DbRequestBuilder (fieldNames, mutateRequest,
|
|||||||
readRequest)
|
readRequest)
|
||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
import PostgREST.Error (PgError (..), SimpleError (..),
|
import PostgREST.Error (PgError (..), SimpleError (..),
|
||||||
errorResponseFor)
|
errorResponseFor, singularityError)
|
||||||
import PostgREST.Middleware
|
import PostgREST.Middleware
|
||||||
import PostgREST.OpenAPI
|
import PostgREST.OpenAPI
|
||||||
import PostgREST.Parsers (pRequestColumns)
|
import PostgREST.Parsers (pRequestColumns)
|
||||||
@@ -275,7 +275,7 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
callProc qi (specifiedProcArgs cols proc) returnsScalar q cq shouldCount
|
callProc qi (specifiedProcArgs cols proc) returnsScalar q cq shouldCount
|
||||||
singular (iPreferSingleObjectParameter apiRequest)
|
singular (iPreferSingleObjectParameter apiRequest)
|
||||||
(contentType == CTTextCSV)
|
(contentType == CTTextCSV)
|
||||||
(contentType == CTOctetStream) bField
|
(contentType `elem` rawContentTypes) bField
|
||||||
(pgVersion dbStructure)
|
(pgVersion dbStructure)
|
||||||
let (tableTotal, queryTotal, body, jsonHeaders) =
|
let (tableTotal, queryTotal, body, jsonHeaders) =
|
||||||
fromMaybe (Just 0, 0, "[]", "[]") row
|
fromMaybe (Just 0, 0, "[]", "[]") row
|
||||||
@@ -334,11 +334,12 @@ responseContentTypeOrError :: [ContentType] -> Action -> Target -> Either Respon
|
|||||||
responseContentTypeOrError accepts action target = serves contentTypesForRequest accepts
|
responseContentTypeOrError accepts action target = serves contentTypesForRequest accepts
|
||||||
where
|
where
|
||||||
contentTypesForRequest = case action of
|
contentTypesForRequest = case action of
|
||||||
ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream]
|
ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ++ rawContentTypes
|
||||||
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream] ++ [CTOpenAPI | tpIsRootSpec target]
|
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ++ rawContentTypes ++
|
||||||
|
[CTOpenAPI | tpIsRootSpec target]
|
||||||
ActionInspect -> [CTOpenAPI, CTApplicationJSON]
|
ActionInspect -> [CTOpenAPI, CTApplicationJSON]
|
||||||
ActionInfo -> [CTTextCSV]
|
ActionInfo -> [CTTextCSV]
|
||||||
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
@@ -347,14 +348,18 @@ responseContentTypeOrError accepts action target = serves contentTypesForRequest
|
|||||||
Nothing -> Left . errorResponseFor . ContentTypeError . map toMime $ cAccepts
|
Nothing -> Left . errorResponseFor . ContentTypeError . map toMime $ cAccepts
|
||||||
Just ct -> Right ct
|
Just ct -> Right ct
|
||||||
|
|
||||||
|
{-
|
||||||
|
| 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 :: ContentType -> [FieldName] -> Either Response (Maybe FieldName)
|
binaryField :: ContentType -> [FieldName] -> Either Response (Maybe FieldName)
|
||||||
binaryField CTOctetStream fldNames =
|
binaryField ct fldNames
|
||||||
if length fldNames == 1 && fieldName /= Just "*"
|
| ct `elem` rawContentTypes =
|
||||||
then Right fieldName
|
let fieldName = headMay fldNames in
|
||||||
else Left . errorResponseFor $ BinaryFieldError
|
if length fldNames == 1 && fieldName /= Just "*"
|
||||||
where
|
then Right fieldName
|
||||||
fieldName = headMay fldNames
|
else Left . errorResponseFor $ BinaryFieldError ct
|
||||||
binaryField _ _ = Right Nothing
|
| otherwise = Right Nothing
|
||||||
|
|
||||||
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString)
|
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString)
|
||||||
splitKeyValue kv = (k, BS.tail v)
|
splitKeyValue kv = (k, BS.tail v)
|
||||||
@@ -385,6 +390,3 @@ contentRangeH lower upper total =
|
|||||||
|
|
||||||
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
|
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
|
||||||
extractQueryResult = fromMaybe (Nothing, 0, [], "")
|
extractQueryResult = fromMaybe (Nothing, 0, [], "")
|
||||||
|
|
||||||
singularityError :: (Integral a) => a -> SimpleError
|
|
||||||
singularityError = SingularityError . toInteger
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ module PostgREST.Error (
|
|||||||
, SimpleError(..)
|
, SimpleError(..)
|
||||||
, errorPayload
|
, errorPayload
|
||||||
, checkIsFatal
|
, checkIsFatal
|
||||||
|
, singularityError
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -190,7 +191,7 @@ checkIsFatal _ = Nothing
|
|||||||
|
|
||||||
data SimpleError
|
data SimpleError
|
||||||
= GucHeadersError
|
= GucHeadersError
|
||||||
| BinaryFieldError
|
| BinaryFieldError ContentType
|
||||||
| ConnectionLostError
|
| ConnectionLostError
|
||||||
| PutSingletonError
|
| PutSingletonError
|
||||||
| PutMatchingPkError
|
| PutMatchingPkError
|
||||||
@@ -204,7 +205,7 @@ data SimpleError
|
|||||||
|
|
||||||
instance PgrstError SimpleError where
|
instance PgrstError SimpleError where
|
||||||
status GucHeadersError = HT.status500
|
status GucHeadersError = HT.status500
|
||||||
status BinaryFieldError = HT.status406
|
status (BinaryFieldError _) = HT.status406
|
||||||
status ConnectionLostError = HT.status503
|
status ConnectionLostError = HT.status503
|
||||||
status PutSingletonError = HT.status400
|
status PutSingletonError = HT.status400
|
||||||
status PutMatchingPkError = HT.status400
|
status PutMatchingPkError = HT.status400
|
||||||
@@ -222,8 +223,8 @@ instance PgrstError SimpleError where
|
|||||||
instance JSON.ToJSON SimpleError where
|
instance JSON.ToJSON SimpleError where
|
||||||
toJSON GucHeadersError = JSON.object [
|
toJSON GucHeadersError = JSON.object [
|
||||||
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)]
|
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)]
|
||||||
toJSON BinaryFieldError = JSON.object [
|
toJSON (BinaryFieldError ct) = JSON.object [
|
||||||
"message" .= ((toS (toMime CTOctetStream) <> " requested but a single column was not selected") :: Text)]
|
"message" .= ((toS (toMime ct) <> " requested but a single column was not selected") :: Text)]
|
||||||
toJSON ConnectionLostError = JSON.object [
|
toJSON ConnectionLostError = JSON.object [
|
||||||
"message" .= ("Database connection lost, retrying the connection." :: Text)]
|
"message" .= ("Database connection lost, retrying the connection." :: Text)]
|
||||||
|
|
||||||
@@ -250,3 +251,6 @@ instance JSON.ToJSON SimpleError where
|
|||||||
invalidTokenHeader :: Text -> Header
|
invalidTokenHeader :: Text -> Header
|
||||||
invalidTokenHeader m =
|
invalidTokenHeader m =
|
||||||
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m)
|
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m)
|
||||||
|
|
||||||
|
singularityError :: (Integral a) => a -> SimpleError
|
||||||
|
singularityError = SingularityError . toInteger
|
||||||
|
|||||||
+31
-8
@@ -6,11 +6,13 @@ Description : PostgREST common types and functions used by the rest of the modul
|
|||||||
|
|
||||||
module PostgREST.Types where
|
module PostgREST.Types where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.ByteString.Internal as BS (c2w)
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Data.Set as S
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import qualified Data.Set as S
|
||||||
import qualified GHC.Show
|
import qualified GHC.Show
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, hContentType)
|
import Network.HTTP.Types.Header (Header, hContentType)
|
||||||
@@ -21,9 +23,10 @@ import PostgREST.RangeQuery (NonnegRange)
|
|||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
-- | Enumeration of currently supported response content types
|
-- | Enumeration of currently supported response content types
|
||||||
data ContentType = CTApplicationJSON | CTTextCSV | CTOpenAPI
|
data ContentType = CTApplicationJSON | CTSingularJSON
|
||||||
| CTSingularJSON | CTOctetStream
|
| CTTextCSV | CTTextPlain | CTTextHtml
|
||||||
| CTAny | CTOther ByteString deriving Eq
|
| CTOpenAPI | CTOctetStream
|
||||||
|
| CTAny | CTOther ByteString deriving (Show, Eq)
|
||||||
|
|
||||||
-- | Convert from ContentType to a full HTTP Header
|
-- | Convert from ContentType to a full HTTP Header
|
||||||
toHeader :: ContentType -> Header
|
toHeader :: ContentType -> Header
|
||||||
@@ -33,12 +36,32 @@ toHeader ct = (hContentType, toMime ct <> "; charset=utf-8")
|
|||||||
toMime :: ContentType -> ByteString
|
toMime :: ContentType -> ByteString
|
||||||
toMime CTApplicationJSON = "application/json"
|
toMime CTApplicationJSON = "application/json"
|
||||||
toMime CTTextCSV = "text/csv"
|
toMime CTTextCSV = "text/csv"
|
||||||
|
toMime CTTextPlain = "text/plain"
|
||||||
|
toMime CTTextHtml = "text/html"
|
||||||
toMime CTOpenAPI = "application/openapi+json"
|
toMime CTOpenAPI = "application/openapi+json"
|
||||||
toMime CTSingularJSON = "application/vnd.pgrst.object+json"
|
toMime CTSingularJSON = "application/vnd.pgrst.object+json"
|
||||||
toMime CTOctetStream = "application/octet-stream"
|
toMime CTOctetStream = "application/octet-stream"
|
||||||
toMime CTAny = "*/*"
|
toMime CTAny = "*/*"
|
||||||
toMime (CTOther ct) = ct
|
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
|
||||||
|
"text/csv" -> CTTextCSV
|
||||||
|
"text/plain" -> CTTextPlain
|
||||||
|
"text/html" -> CTTextHtml
|
||||||
|
"application/openapi+json" -> CTOpenAPI
|
||||||
|
"application/vnd.pgrst.object+json" -> CTSingularJSON
|
||||||
|
"application/vnd.pgrst.object" -> CTSingularJSON
|
||||||
|
"application/octet-stream" -> CTOctetStream
|
||||||
|
"*/*" -> CTAny
|
||||||
|
ct' -> CTOther ct'
|
||||||
|
|
||||||
|
-- | ContentTypes that can get a raw/unwrapped response
|
||||||
|
rawContentTypes :: [ContentType]
|
||||||
|
rawContentTypes = [CTOctetStream, CTTextPlain, CTTextHtml]
|
||||||
|
|
||||||
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
|
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
|
||||||
instance Show PreferResolution where
|
instance Show PreferResolution where
|
||||||
show MergeDuplicates = "resolution=merge-duplicates"
|
show MergeDuplicates = "resolution=merge-duplicates"
|
||||||
|
|||||||
+30
-49
@@ -895,58 +895,39 @@ spec = do
|
|||||||
[json|[{":arr->ow::cast":" arrow-1 ","(inside,parens)":" parens-1 ","a.dotted.column":" dotted-1 "," col w space ":" space-1"}]|]
|
[json|[{":arr->ow::cast":" arrow-1 ","(inside,parens)":" parens-1 ","a.dotted.column":" dotted-1 "," col w space ":" space-1"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
describe "binary output" $ do
|
context "binary output" $ do
|
||||||
context "on GET" $ do
|
it "can query if a single column is selected" $
|
||||||
it "can query if a single column is selected" $
|
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||||
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
||||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
{ matchStatus = 200
|
||||||
{ matchStatus = 200
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
}
|
||||||
}
|
|
||||||
|
|
||||||
it "fails if a single column is not selected" $ do
|
it "can get raw output with Accept: text/plain" $
|
||||||
request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
request methodGet "/projects?select=name&id=eq.1" (acceptHdrs "text/plain") ""
|
||||||
`shouldRespondWith`
|
`shouldRespondWith` "Windows 7"
|
||||||
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
|
{ matchStatus = 200
|
||||||
{ matchStatus = 406
|
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
||||||
, matchHeaders = [matchContentTypeJson]
|
}
|
||||||
}
|
|
||||||
request methodGet "/images?select=*&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
|
||||||
`shouldRespondWith` 406
|
|
||||||
request methodGet "/images?name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
|
||||||
`shouldRespondWith` 406
|
|
||||||
|
|
||||||
it "concatenates results if more than one row is returned" $
|
it "fails if a single column is not selected" $ do
|
||||||
request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") ""
|
request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
`shouldRespondWith`
|
||||||
{ matchStatus = 200
|
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
|
||||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
{ matchStatus = 406
|
||||||
}
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
request methodGet "/images?select=*&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` 406
|
||||||
|
request methodGet "/images?name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` 406
|
||||||
|
|
||||||
context "on RPC" $ do
|
it "concatenates results if more than one row is returned" $
|
||||||
context "Proc that returns scalar" $
|
request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") ""
|
||||||
it "can query without selecting column" $
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
||||||
request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""
|
{ matchStatus = 200
|
||||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
{ matchStatus = 200
|
}
|
||||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
|
||||||
}
|
|
||||||
|
|
||||||
context "Proc that returns rows" $ do
|
|
||||||
it "can query if a single column is selected" $
|
|
||||||
request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""
|
|
||||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "fails if a single column is not selected" $
|
|
||||||
request methodPost "/rpc/ret_rows_with_base64_bin" (acceptHdrs "application/octet-stream") ""
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
|
|
||||||
{ matchStatus = 406
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
describe "HTTP request env vars" $ do
|
describe "HTTP request env vars" $ do
|
||||||
it "custom header is set" $
|
it "custom header is set" $
|
||||||
|
|||||||
@@ -469,6 +469,55 @@ spec actualPgVersion =
|
|||||||
{"id":4,"name":"OSX"}]
|
{"id":4,"name":"OSX"}]
|
||||||
|] { matchHeaders = [matchContentTypeJson] }
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "binary output" $ do
|
||||||
|
context "Proc that returns scalar" $ do
|
||||||
|
it "can query without selecting column" $
|
||||||
|
request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "can get raw output with Accept: text/html" $
|
||||||
|
request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[str|
|
||||||
|
|<html>
|
||||||
|
| <head>
|
||||||
|
| <title>PostgREST</title>
|
||||||
|
| </head>
|
||||||
|
| <body>
|
||||||
|
| <h1>Welcome to PostgREST</h1>
|
||||||
|
| </body>
|
||||||
|
|</html>
|
||||||
|
|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "text/html; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "can get raw output with Accept: text/plain" $
|
||||||
|
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
|
||||||
|
`shouldRespondWith` "Welcome to PostgREST"
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
context "Proc that returns rows" $ do
|
||||||
|
it "can query if a single column is selected" $
|
||||||
|
request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "fails if a single column is not selected" $
|
||||||
|
request methodPost "/rpc/ret_rows_with_base64_bin" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
|
||||||
|
{ matchStatus = 406
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
context "only for GET rpc" $ do
|
context "only for GET rpc" $ do
|
||||||
it "should fail on mutating procs" $ do
|
it "should fail on mutating procs" $ do
|
||||||
get "/rpc/callcounter" `shouldRespondWith` 500
|
get "/rpc/callcounter" `shouldRespondWith` 500
|
||||||
|
|||||||
Vendored
+17
@@ -1713,3 +1713,20 @@ case current_setting('request.header.accept', true)
|
|||||||
end case;
|
end case;
|
||||||
end
|
end
|
||||||
$_$ language plpgsql;
|
$_$ language plpgsql;
|
||||||
|
|
||||||
|
create or replace function welcome() returns text as $$
|
||||||
|
select 'Welcome to PostgREST'::text;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function "welcome.html"() returns text as $_$
|
||||||
|
select $$
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>PostgREST</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome to PostgREST</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
$$::text;
|
||||||
|
$_$ language sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user