Add response.headers on GET/POST/PATCH/PUT/DELETE

This commit is contained in:
steve-chavez
2020-01-21 12:28:05 -05:00
committed by Steve Chavez
parent 7dade7f466
commit c7f78fa7fc
9 changed files with 202 additions and 115 deletions
+3 -2
View File
@@ -13,8 +13,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez - #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez
- #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst - #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst
- #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez - #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez
- #1168, Allow access to the Authorization header through the request.header.authorization GUC - @steve-chavez - #1168, Allow access to the `Authorization` header through the `request.header.authorization` GUC - @steve-chavez
- #1435, Add request.method and request.path GUCs - @steve-chavez - #1435, Add `request.method` and `request.path` GUCs - @steve-chavez
- #1088, Allow adding headers to GET/POST/PATCH/PUT/DELETE responses through the `response.headers` GUC - @steve-chavez
### Fixed ### Fixed
+99 -87
View File
@@ -131,24 +131,26 @@ app dbStructure proc cols conf apiRequest =
then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
else cq else cq
stm = createReadStatement q cQuery (contentType == CTSingularJSON) shouldCount stm = createReadStatement q cQuery (contentType == CTSingularJSON) shouldCount
(contentType == CTTextCSV) bField (contentType == CTTextCSV) bField pgVer
explStm = createExplainStatement cq explStm = createExplainStatement cq
row <- H.statement () stm row <- H.statement () stm
let (tableTotal, queryTotal, _ , body) = row let (tableTotal, queryTotal, _ , body, gucHeaders) = row
total <- if | plannedCount -> H.statement () explStm case gucHeaders of
| estimatedCount -> if tableTotal > (fromIntegral <$> maxRows) Left _ -> return . errorResponseFor $ GucHeadersError
then do estTotal <- H.statement () explStm Right hs -> do
pure $ if estTotal > tableTotal then estTotal else tableTotal total <- if | plannedCount -> H.statement () explStm
else pure tableTotal | estimatedCount -> if tableTotal > (fromIntegral <$> maxRows)
| otherwise -> pure tableTotal then do estTotal <- H.statement () explStm
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total pure $ if estTotal > tableTotal then estTotal else tableTotal
return $ else pure tableTotal
if contentType == CTSingularJSON && queryTotal /= 1 | otherwise -> pure tableTotal
then errorResponseFor . singularityError $ queryTotal let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total
else responseLBS status return $
[toHeader contentType, contentRange, if contentType == CTSingularJSON && queryTotal /= 1
contentLocationH tName (iCanonicalQS apiRequest)] then errorResponseFor . singularityError $ queryTotal
(if headersOnly then mempty else toS body) else responseLBS status
([toHeader contentType, contentRange, contentLocationH tName (iCanonicalQS apiRequest)] ++ (gucHToHeader <$> hs))
(if headersOnly then mempty else toS body)
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) -> (ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -157,31 +159,31 @@ app dbStructure proc cols conf apiRequest =
let pkCols = tablePKCols dbStructure tSchema tName let pkCols = tablePKCols dbStructure tSchema tName
stm = createWriteStatement sq mq stm = createWriteStatement sq mq
(contentType == CTSingularJSON) True (contentType == CTSingularJSON) True
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols (contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols pgVer
row <- H.statement (toS $ pjRaw pJson) stm row <- H.statement (toS $ pjRaw pJson) stm
let (_, queryTotal, fs, body) = row let (_, queryTotal, fs, body, gucHeaders) = row
headers = catMaybes [ case gucHeaders of
if null fs Left _ -> return . errorResponseFor $ GucHeadersError
then Nothing Right hdrs -> do
else Just $ locationH tName fs let headers = catMaybes [
, if iPreferRepresentation apiRequest == Full if null fs
then Just $ toHeader contentType then Nothing
else Nothing else Just $ locationH tName fs
, Just $ contentRangeH 1 0 $ , if iPreferRepresentation apiRequest == Full
if shouldCount then Just queryTotal else Nothing then Just $ toHeader contentType
, if null pkCols && isNothing (iOnConflict apiRequest) else Nothing
then Nothing , Just $ contentRangeH 1 0 $
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest if shouldCount then Just queryTotal else Nothing
] , if null pkCols && isNothing (iOnConflict apiRequest)
if contentType == CTSingularJSON then Nothing
&& queryTotal /= 1 else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
then do ] ++ (gucHToHeader <$> hdrs)
HT.condemn if contentType == CTSingularJSON && queryTotal /= 1
return . errorResponseFor . singularityError $ queryTotal then do
else HT.condemn
return . responseLBS status201 headers $ return . errorResponseFor . singularityError $ queryTotal
if iPreferRepresentation apiRequest == Full else
then toS body else "" return . responseLBS status201 headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) -> (ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -189,26 +191,27 @@ app dbStructure proc cols conf apiRequest =
Right (sq, mq) -> do Right (sq, mq) -> do
let stm = createWriteStatement sq mq let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTTextCSV) (contentType == CTSingularJSON) False (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) [] (iPreferRepresentation apiRequest) [] pgVer
row <- H.statement (toS $ pjRaw pJson) stm row <- H.statement (toS $ pjRaw pJson) stm
let (_, queryTotal, _, body) = row let (_, queryTotal, _, body, gucHeaders) = row
updateIsNoOp = S.null cols case gucHeaders of
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ Left _ -> return . errorResponseFor $ GucHeadersError
if shouldCount then Just queryTotal else Nothing Right hdrs -> do
headers | iPreferRepresentation apiRequest == Full = [toHeader contentType, contentRangeHeader] let
| otherwise = [contentRangeHeader] updateIsNoOp = S.null cols
status | queryTotal == 0 && not updateIsNoOp = status404 status | queryTotal == 0 && not updateIsNoOp = status404
| iPreferRepresentation apiRequest == Full = status200 | iPreferRepresentation apiRequest == Full = status200
| otherwise = status204 | otherwise = status204
if contentType == CTSingularJSON contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing
&& queryTotal /= 1 headers = [contentRangeHeader] ++
then do [if iPreferRepresentation apiRequest == Full then toHeader contentType else mempty] ++
HT.condemn (gucHToHeader <$> hdrs)
return . errorResponseFor . singularityError $ queryTotal if contentType == CTSingularJSON && queryTotal /= 1
else then do
return $ if iPreferRepresentation apiRequest == Full HT.condemn
then responseLBS status headers (toS body) return . errorResponseFor . singularityError $ queryTotal
else responseLBS status headers mempty else
return . responseLBS status headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) -> (ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -227,19 +230,22 @@ app dbStructure proc cols conf apiRequest =
else do else do
row <- H.statement (toS pjRaw) $ row <- H.statement (toS pjRaw) $
createWriteStatement sq mq (contentType == CTSingularJSON) False createWriteStatement sq mq (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] (contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer
let (_, queryTotal, _, body) = row let (_, queryTotal, _, body, gucHeaders) = row
-- Makes sure the querystring pk matches the payload pk case gucHeaders of
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected Left _ -> return . errorResponseFor $ GucHeadersError
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done Right hdrs -> do
if queryTotal /= 1 let headers = toHeader contentType : (gucHToHeader <$> hdrs)
then do status = if iPreferRepresentation apiRequest == Full then status200 else status204
HT.condemn -- Makes sure the querystring pk matches the payload pk
return . errorResponseFor $ PutMatchingPkError -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
else -- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done
return $ if iPreferRepresentation apiRequest == Full if queryTotal /= 1
then responseLBS status200 [toHeader contentType] (toS body) then do
else responseLBS status204 [] "" HT.condemn
return . errorResponseFor $ PutMatchingPkError
else
return . responseLBS status headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) -> (ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -248,20 +254,25 @@ app dbStructure proc cols conf apiRequest =
let stm = createWriteStatement sq mq let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) [] (iPreferRepresentation apiRequest) [] pgVer
row <- H.statement mempty stm row <- H.statement mempty stm
let (_, queryTotal, _, body) = row let (_, queryTotal, _, body, gucHeaders) = row
contentRangeHeader = contentRangeH 1 0 $ case gucHeaders of
if shouldCount then Just queryTotal else Nothing Left _ -> return . errorResponseFor $ GucHeadersError
if contentType == CTSingularJSON Right hdrs -> do
&& queryTotal /= 1 let
then do status = if iPreferRepresentation apiRequest == Full then status200 else status204
HT.condemn contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
return . errorResponseFor . singularityError $ queryTotal headers = [contentRangeHeader] ++
else [if iPreferRepresentation apiRequest == Full then toHeader contentType else mempty] ++
return $ if iPreferRepresentation apiRequest == Full (gucHToHeader <$> hdrs)
then responseLBS status200 [toHeader contentType, contentRangeHeader] (toS body) if contentType == CTSingularJSON
else responseLBS status204 [contentRangeHeader] "" && queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return . responseLBS status headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) -> (ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in
@@ -282,7 +293,7 @@ app dbStructure proc cols conf apiRequest =
pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams
stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON) stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON)
(contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects) (contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects)
bField (pgVersion dbStructure) bField pgVer
row <- H.statement (toS $ pjRaw pJson) stm row <- H.statement (toS $ pjRaw pJson) stm
let (tableTotal, queryTotal, body, gucHeaders) = row let (tableTotal, queryTotal, body, gucHeaders) = row
(status, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal (status, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal
@@ -294,7 +305,7 @@ app dbStructure proc cols conf apiRequest =
HT.condemn HT.condemn
return . errorResponseFor . singularityError $ queryTotal return . errorResponseFor . singularityError $ queryTotal
else else
return $ responseLBS status ([toHeader contentType, contentRange] ++ toHeaders hs) return $ responseLBS status ([toHeader contentType, contentRange] ++ (gucHToHeader <$> hs))
(if invMethod == InvHead then mempty else toS body) (if invMethod == InvHead then mempty else toS body)
(ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do (ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do
@@ -325,6 +336,7 @@ app dbStructure proc cols conf apiRequest =
shouldCount = exactCount || estimatedCount shouldCount = exactCount || estimatedCount
topLevelRange = iTopLevelRange apiRequest topLevelRange = iTopLevelRange apiRequest
returnsScalar = maybe False procReturnsScalar proc returnsScalar = maybe False procReturnsScalar proc
pgVer = pgVersion dbStructure
readSqlParts s t = readSqlParts s t =
let let
+6
View File
@@ -197,3 +197,9 @@ returningF qi returnings =
if null returnings if null returnings
then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified
else "RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings) else "RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings)
responseHeadersF :: PgVersion -> SqlFragment
responseHeadersF pgVer =
if pgVer >= pgVersion96
then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
else "'[]'" :: Text
+18 -19
View File
@@ -38,12 +38,12 @@ import Text.InterpolatedString.Perl6 (qc)
is represented as a list of strings containing variable bindings like is represented as a list of strings containing variable bindings like
@"k1=eq.42"@, or the empty list if there is no location header. @"k1=eq.42"@, or the empty list if there is no location header.
-} -}
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString) type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Text [GucHeader])
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] -> PreferRepresentation -> [Text] -> PgVersion ->
H.Statement ByteString ResultsWithCount H.Statement ByteString ResultsWithCount
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys = createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys pgVer =
unicodeStatement sql (param HE.unknown) decodeStandard True unicodeStatement sql (param HE.unknown) decodeStandard True
where where
sql = [qc| sql = [qc|
@@ -53,7 +53,8 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
'' AS total_result_set, '' AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total, pg_catalog.count(_postgrest_t) AS page_total,
{locF} AS header, {locF} AS header,
{bodyF} AS body {bodyF} AS body,
{responseHeadersF pgVer} AS response_headers
FROM ({selectQuery}) _postgrest_t |] FROM ({selectQuery}) _postgrest_t |]
locF = locF =
@@ -73,11 +74,11 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
decodeStandard :: HD.Result ResultsWithCount decodeStandard :: HD.Result ResultsWithCount
decodeStandard = decodeStandard =
fromMaybe (Nothing, 0, [], "") <$> HD.rowMaybe standardRow fromMaybe (Nothing, 0, [], mempty, Right []) <$> HD.rowMaybe standardRow
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName -> createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion ->
H.Statement () ResultsWithCount H.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField = createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField pgVer =
unicodeStatement sql HE.noParams decodeStandard False unicodeStatement sql HE.noParams decodeStandard False
where where
sql = [qc| sql = [qc|
@@ -88,7 +89,8 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField
{countResultF} AS total_result_set, {countResultF} AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total, pg_catalog.count(_postgrest_t) AS page_total,
{noLocationF} AS header, {noLocationF} AS header,
{bodyF} AS body {bodyF} AS body,
{responseHeadersF pgVer} AS response_headers
FROM ( SELECT * FROM {sourceCTEName}) _postgrest_t |] FROM ( SELECT * FROM {sourceCTEName}) _postgrest_t |]
(countCTEF, countResultF) = countF countQuery countTotal (countCTEF, countResultF) = countF countQuery countTotal
@@ -108,8 +110,8 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField
for that common type of query. for that common type of query.
-} -}
standardRow :: HD.Row ResultsWithCount standardRow :: HD.Row ResultsWithCount
standardRow = (,,,) <$> nullableColumn HD.int8 <*> column HD.int8 standardRow = (,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> column header <*> column HD.bytea <*> column header <*> column HD.bytea <*> column decodeGucHeaders
where where
header = HD.array $ HD.dimension replicateM $ element HD.bytea header = HD.array $ HD.dimension replicateM $ element HD.bytea
@@ -128,7 +130,7 @@ callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal
{countResultF} AS total_result_set, {countResultF} AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total, pg_catalog.count(_postgrest_t) AS page_total,
{bodyF} AS body, {bodyF} AS body,
{responseHeaders} AS response_headers {responseHeadersF pgVer} AS response_headers
FROM ({selectQuery}) _postgrest_t;|] FROM ({selectQuery}) _postgrest_t;|]
(countCTEF, countResultF) = countF countQuery countTotal (countCTEF, countResultF) = countF countQuery countTotal
@@ -145,18 +147,12 @@ callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal
| multObjects = "json_agg(_postgrest_t.pgrst_scalar)::character varying" | multObjects = "json_agg(_postgrest_t.pgrst_scalar)::character varying"
| otherwise = "(json_agg(_postgrest_t.pgrst_scalar)->0)::character varying" | otherwise = "(json_agg(_postgrest_t.pgrst_scalar)->0)::character varying"
responseHeaders =
if pgVer >= pgVersion96
then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
else "'[]'" :: Text
decodeProc :: HD.Result ProcResults decodeProc :: HD.Result ProcResults
decodeProc = decodeProc =
let row = fromMaybe (Just 0, 0, "[]", "[]") <$> HD.rowMaybe procRow in fromMaybe (Just 0, 0, mempty, Right []) <$> HD.rowMaybe procRow
(\(a, b, c, d) -> (a, b, c, first toS $ JSON.eitherDecode $ toS d)) <$> row
where where
procRow = (,,,) <$> nullableColumn HD.int8 <*> column HD.int8 procRow = (,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> column HD.bytea <*> column HD.bytea <*> column HD.bytea <*> column decodeGucHeaders
createExplainStatement :: SqlQuery -> H.Statement () (Maybe Int64) createExplainStatement :: SqlQuery -> H.Statement () (Maybe Int64)
createExplainStatement countQuery = createExplainStatement countQuery =
@@ -178,3 +174,6 @@ createExplainStatement countQuery =
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Statement a b unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Statement a b
unicodeStatement = H.Statement . encodeUtf8 unicodeStatement = H.Statement . encodeUtf8
decodeGucHeaders :: HD.Value (Either Text [GucHeader])
decodeGucHeaders = first toS . JSON.eitherDecode . toS <$> HD.bytea
+2 -2
View File
@@ -408,8 +408,8 @@ instance JSON.FromJSON GucHeader where
_ -> mzero _ -> mzero
parseJSON _ = mzero parseJSON _ = mzero
toHeaders :: [GucHeader] -> [Header] gucHToHeader :: GucHeader -> Header
toHeaders = map $ \(GucHeader (k, v)) -> (CI.mk $ toS k, toS v) gucHToHeader (GucHeader (k, v)) = (CI.mk $ toS k, toS v)
{-| {-|
This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones. This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones.
+47 -2
View File
@@ -1,6 +1,7 @@
module Feature.PgVersion96Spec where module Feature.PgVersion96Spec where
import Network.Wai (Application) import Network.HTTP.Types
import Network.Wai (Application)
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
@@ -12,7 +13,7 @@ import SpecHelper
spec :: SpecWith Application spec :: SpecWith Application
spec = spec =
describe "features supported on PostgreSQL 9.6" $ do describe "features supported on PostgreSQL 9.6" $ do
context "GUC headers" $ do context "GUC headers on function calls" $ do
it "succeeds setting the headers" $ do it "succeeds setting the headers" $ do
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id" get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
`shouldRespondWith` [json|[{"id": 2}]|] `shouldRespondWith` [json|[{"id": 2}]|]
@@ -67,6 +68,50 @@ spec =
"Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/", "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/",
"Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]} "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]}
context "GUC headers on all other methods via pre-request" $ do
it "succeeds setting the headers on GET and HEAD" $ do
request methodGet "/items?id=eq.1" [("User-Agent", "MSIE 6.0")] mempty
`shouldRespondWith` [json|[{"id": 1}]|]
{matchHeaders = [
matchContentTypeJson,
"Cache-Control" <:> "no-cache, no-store, must-revalidate"]}
request methodHead "/items?id=eq.1" [("User-Agent", "MSIE 7.0")] mempty
`shouldRespondWith` ""
{matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"]}
request methodHead "/projects" [("Accept", "text/csv")] mempty
`shouldRespondWith` ""
{matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"]}
it "succeeds setting the headers on POST" $
request methodPost "/items" [] [json|[{"id": 11111}]|]
`shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on PATCH" $
request methodPatch "/items?id=eq.11111" [] [json|[{"id": 11111}]|]
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on PUT" $
request methodPut "/items?id=eq.11111" [] [json|[{"id": 11111}]|]
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on DELETE" $
request methodDelete "/items?id=eq.11111" [] mempty
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
context "Use of the phraseto_tsquery function" $ do context "Use of the phraseto_tsquery function" $ do
it "finds matches" $ it "finds matches" $
get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith` get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`
+5 -3
View File
@@ -78,6 +78,7 @@ main = do
extraSearchPathApp = return $ postgrest (testCfgExtraSearchPath testDbConn) refDbStructure pool getTime $ pure () extraSearchPathApp = return $ postgrest (testCfgExtraSearchPath testDbConn) refDbStructure pool getTime $ pure ()
rootSpecApp = return $ postgrest (testCfgRootSpec testDbConn) refDbStructure pool getTime $ pure () rootSpecApp = return $ postgrest (testCfgRootSpec testDbConn) refDbStructure pool getTime $ pure ()
htmlRawOutputApp = return $ postgrest (testCfgHtmlRawOutput testDbConn) refDbStructure pool getTime $ pure () htmlRawOutputApp = return $ postgrest (testCfgHtmlRawOutput testDbConn) refDbStructure pool getTime $ pure ()
responseHeadersApp = return $ postgrest (testCfgResponseHeaders testDbConn) refDbStructure pool getTime $ pure ()
let reset, analyze :: IO () let reset, analyze :: IO ()
reset = resetDb testDbConn reset = resetDb testDbConn
@@ -88,8 +89,7 @@ main = do
actualPgVersion = pgVersion dbStructure actualPgVersion = pgVersion dbStructure
extraSpecs = extraSpecs =
[("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++ [("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++
[("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95] ++ [("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95]
[("Feature.PgVersion96Spec", Feature.PgVersion96Spec.spec) | actualPgVersion >= pgVersion96]
specs = uncurry describe <$> [ specs = uncurry describe <$> [
("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion) ("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion)
@@ -165,6 +165,8 @@ main = do
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
-- this test runs with a root spec function override -- this test runs with a root spec function override
when (actualPgVersion >= pgVersion96) $ when (actualPgVersion >= pgVersion96) $ do
before rootSpecApp $ before rootSpecApp $
describe "Feature.RootSpec" Feature.RootSpec.spec describe "Feature.RootSpec" Feature.RootSpec.spec
before responseHeadersApp $
describe "Feature.PgVersion96Spec" Feature.PgVersion96Spec.spec
+3
View File
@@ -138,6 +138,9 @@ testCfgRootSpec testDbConn = (testCfg testDbConn) { configRootSpec = Just "root"
testCfgHtmlRawOutput :: Text -> AppConfig testCfgHtmlRawOutput :: Text -> AppConfig
testCfgHtmlRawOutput testDbConn = (testCfg testDbConn) { configRawMediaTypes = ["text/html"] } testCfgHtmlRawOutput testDbConn = (testCfg testDbConn) { configRawMediaTypes = ["text/html"] }
testCfgResponseHeaders :: Text -> AppConfig
testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configReqCheck = Just "custom_headers" }
setupDb :: Text -> IO () setupDb :: Text -> IO ()
setupDb dbConn = do setupDb dbConn = do
loadFixture dbConn "database" loadFixture dbConn "database"
+19
View File
@@ -1642,3 +1642,22 @@ add constraint fst_shift foreign key (fst_shift_activity_id, fst_shift
references activities (id, schedule_id), references activities (id, schedule_id),
add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift_schedule_id) add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift_schedule_id)
references activities (id, schedule_id); references activities (id, schedule_id);
-- for a pre-request function
create or replace function custom_headers() returns void as $$
declare
user_agent text := current_setting('request.header.user-agent', true);
req_path text := current_setting('request.path', true);
req_accept text := current_setting('request.header.accept', true);
begin
if user_agent similar to 'MSIE (6.0|7.0)' then
perform set_config('response.headers',
'[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false);
elsif req_path similar to '/(items|projects)' and req_accept = 'text/csv' then
perform set_config('response.headers',
format('[{"Content-Disposition": "attachment; filename=%s.csv"}]', trim('/' from req_path)), false);
else
perform set_config('response.headers',
'[{"X-Custom-Header": "mykey=myval"}]', false);
end if;
end; $$ language plpgsql;