Allow overriding headers Location,Content-Type,etc

This commit is contained in:
steve-chavez
2020-01-21 12:28:05 -05:00
committed by Steve Chavez
parent c7f78fa7fc
commit 9b1224827a
6 changed files with 98 additions and 42 deletions
+1
View File
@@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #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 - #1088, Allow adding headers to GET/POST/PATCH/PUT/DELETE responses through the `response.headers` GUC - @steve-chavez
- #1427, Allow overriding provided headers(Location, Content-Type, etc) through the `response.headers` GUC - @steve-chavez
### Fixed ### Fixed
+38 -38
View File
@@ -137,7 +137,7 @@ app dbStructure proc cols conf apiRequest =
let (tableTotal, queryTotal, _ , body, gucHeaders) = row let (tableTotal, queryTotal, _ , body, gucHeaders) = row
case gucHeaders of case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hs -> do Right ghdrs -> do
total <- if | plannedCount -> H.statement () explStm total <- if | plannedCount -> H.statement () explStm
| estimatedCount -> if tableTotal > (fromIntegral <$> maxRows) | estimatedCount -> if tableTotal > (fromIntegral <$> maxRows)
then do estTotal <- H.statement () explStm then do estTotal <- H.statement () explStm
@@ -145,12 +145,14 @@ app dbStructure proc cols conf apiRequest =
else pure tableTotal else pure tableTotal
| otherwise -> pure tableTotal | otherwise -> pure tableTotal
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total
headers = addHeadersIfNotIncluded
[toHeader contentType, contentRange, contentLocationH tName (iCanonicalQS apiRequest)]
(unwrapGucHeader <$> ghdrs)
rBody = if headersOnly then mempty else toS body
return $ return $
if contentType == CTSingularJSON && queryTotal /= 1 if contentType == CTSingularJSON && queryTotal /= 1
then errorResponseFor . singularityError $ queryTotal then errorResponseFor . singularityError $ queryTotal
else responseLBS status else responseLBS status headers rBody
([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
@@ -161,29 +163,28 @@ app dbStructure proc cols conf apiRequest =
(contentType == CTSingularJSON) True (contentType == CTSingularJSON) True
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols pgVer (contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols pgVer
row <- H.statement (toS $ pjRaw pJson) stm row <- H.statement (toS $ pjRaw pJson) stm
let (_, queryTotal, fs, body, gucHeaders) = row let (_, queryTotal, fields, body, gucHeaders) = row
case gucHeaders of case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hdrs -> do Right ghdrs -> do
let headers = catMaybes [ let
if null fs (ctHeader, rBody) = if iPreferRepresentation apiRequest == Full then (toHeader contentType, toS body) else (mempty, mempty)
then Nothing headers = addHeadersIfNotIncluded [
else Just $ locationH tName fs if null fields
, if iPreferRepresentation apiRequest == Full then mempty
then Just $ toHeader contentType else locationH tName fields
else Nothing , ctHeader
, Just $ contentRangeH 1 0 $ , contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
if shouldCount then Just queryTotal else Nothing
, if null pkCols && isNothing (iOnConflict apiRequest) , if null pkCols && isNothing (iOnConflict apiRequest)
then Nothing then mempty
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest else maybe mempty (\x -> ("Preference-Applied", show x)) $ iPreferResolution apiRequest
] ++ (gucHToHeader <$> hdrs) ] (unwrapGucHeader <$> ghdrs)
if contentType == CTSingularJSON && queryTotal /= 1 if contentType == CTSingularJSON && queryTotal /= 1
then do then do
HT.condemn HT.condemn
return . errorResponseFor . singularityError $ queryTotal return . errorResponseFor . singularityError $ queryTotal
else else
return . responseLBS status201 headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty return $ responseLBS status201 headers rBody
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) -> (ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -196,22 +197,21 @@ app dbStructure proc cols conf apiRequest =
let (_, queryTotal, _, body, gucHeaders) = row let (_, queryTotal, _, body, gucHeaders) = row
case gucHeaders of case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hdrs -> do Right ghdrs -> do
let let
updateIsNoOp = S.null cols 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
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing
headers = [contentRangeHeader] ++ (ctHeader, rBody) = if iPreferRepresentation apiRequest == Full then (toHeader contentType, toS body) else (mempty, mempty)
[if iPreferRepresentation apiRequest == Full then toHeader contentType else mempty] ++ headers = addHeadersIfNotIncluded [contentRangeHeader, ctHeader] (unwrapGucHeader <$> ghdrs)
(gucHToHeader <$> hdrs)
if contentType == CTSingularJSON && queryTotal /= 1 if contentType == CTSingularJSON && queryTotal /= 1
then do then do
HT.condemn HT.condemn
return . errorResponseFor . singularityError $ queryTotal return . errorResponseFor . singularityError $ queryTotal
else else
return . responseLBS status headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty return $ responseLBS status headers rBody
(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
@@ -234,9 +234,9 @@ app dbStructure proc cols conf apiRequest =
let (_, queryTotal, _, body, gucHeaders) = row let (_, queryTotal, _, body, gucHeaders) = row
case gucHeaders of case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hdrs -> do Right ghdrs -> do
let headers = toHeader contentType : (gucHToHeader <$> hdrs) let headers = addHeadersIfNotIncluded [toHeader contentType] (unwrapGucHeader <$> ghdrs)
status = if iPreferRepresentation apiRequest == Full then status200 else status204 (status, rBody) = if iPreferRepresentation apiRequest == Full then (status200, toS body) else (status204, mempty)
-- Makes sure the querystring pk matches the payload pk -- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done -- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done
@@ -245,7 +245,7 @@ app dbStructure proc cols conf apiRequest =
HT.condemn HT.condemn
return . errorResponseFor $ PutMatchingPkError return . errorResponseFor $ PutMatchingPkError
else else
return . responseLBS status headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty return $ responseLBS status headers rBody
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) -> (ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -259,20 +259,19 @@ app dbStructure proc cols conf apiRequest =
let (_, queryTotal, _, body, gucHeaders) = row let (_, queryTotal, _, body, gucHeaders) = row
case gucHeaders of case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hdrs -> do Right ghdrs -> do
let let
status = if iPreferRepresentation apiRequest == Full then status200 else status204 status = if iPreferRepresentation apiRequest == Full then status200 else status204
contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
headers = [contentRangeHeader] ++ (ctHeader, rBody) = if iPreferRepresentation apiRequest == Full then (toHeader contentType, toS body) else (mempty, mempty)
[if iPreferRepresentation apiRequest == Full then toHeader contentType else mempty] ++ headers = addHeadersIfNotIncluded [contentRangeHeader, ctHeader] (unwrapGucHeader <$> ghdrs)
(gucHToHeader <$> hdrs)
if contentType == CTSingularJSON if contentType == CTSingularJSON
&& queryTotal /= 1 && queryTotal /= 1
then do then do
HT.condemn HT.condemn
return . errorResponseFor . singularityError $ queryTotal return . errorResponseFor . singularityError $ queryTotal
else else
return . responseLBS status headers $ if iPreferRepresentation apiRequest == Full then toS body else mempty return $ responseLBS status headers rBody
(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
@@ -296,17 +295,18 @@ app dbStructure proc cols conf apiRequest =
bField pgVer 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
case gucHeaders of case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hs -> Right ghdrs -> do
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal
headers = addHeadersIfNotIncluded [toHeader contentType, contentRange] (unwrapGucHeader <$> ghdrs)
rBody = if invMethod == InvHead then mempty else toS body
if contentType == CTSingularJSON && queryTotal /= 1 if contentType == CTSingularJSON && queryTotal /= 1
then do then do
HT.condemn HT.condemn
return . errorResponseFor . singularityError $ queryTotal return . errorResponseFor . singularityError $ queryTotal
else else
return $ responseLBS status ([toHeader contentType, contentRange] ++ (gucHToHeader <$> hs)) return $ responseLBS status headers rBody
(if invMethod == InvHead then mempty else toS body)
(ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do (ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do
let host = configHost conf let host = configHost conf
@@ -395,7 +395,7 @@ binaryField ct rawContentTypes isScalarProc readReq
locationH :: TableName -> [BS.ByteString] -> Header locationH :: TableName -> [BS.ByteString] -> Header
locationH tName fields = locationH tName fields =
let let
locationFields = renderSimpleQuery True $ map splitKeyValue fields locationFields = renderSimpleQuery True $ splitKeyValue <$> fields
in in
(hLocation, "/" <> toS tName <> locationFields) (hLocation, "/" <> toS tName <> locationFields)
where where
+11 -4
View File
@@ -399,17 +399,24 @@ type RpcQParam = (Text, Text)
Custom guc header, it's obtained by parsing the json in a: Custom guc header, it's obtained by parsing the json in a:
`SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]' `SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]'
-} -}
newtype GucHeader = GucHeader (Text, Text) newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
deriving (Show, Eq)
instance JSON.FromJSON GucHeader where instance JSON.FromJSON GucHeader where
parseJSON (JSON.Object o) = case headMay (M.toList o) of parseJSON (JSON.Object o) = case headMay (M.toList o) of
Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (k, s) Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (CI.mk $ toS k, toS s)
| otherwise -> mzero | otherwise -> mzero
_ -> mzero _ -> mzero
parseJSON _ = mzero parseJSON _ = mzero
gucHToHeader :: GucHeader -> Header unwrapGucHeader :: GucHeader -> Header
gucHToHeader (GucHeader (k, v)) = (CI.mk $ toS k, toS v) unwrapGucHeader (GucHeader (k, v)) = (k, v)
-- | Add headers not already included to allow the user to override them instead of duplicating them
addHeadersIfNotIncluded :: [Header] -> [Header] -> [Header]
addHeadersIfNotIncluded newHeaders initialHeaders =
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
initialHeaders
{-| {-|
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.
+20
View File
@@ -112,6 +112,26 @@ spec =
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
} }
context "Override provided headers by using GUC headers" $ do
it "can override the Content-Type header" $ do
request methodHead "/clients?id=eq.1" [] mempty
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json"]
}
request methodHead "/rpc/getallprojects" [] mempty
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json"]
}
it "can override the Location header" $
request methodPost "/stuff" [] [json|[{"id": 1, "name": "stuff 1"}]|]
`shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = ["Location" <:> "/stuff?id=eq.1&overriden=true"]
}
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`
+1
View File
@@ -120,6 +120,7 @@ GRANT ALL ON TABLE
, schedules , schedules
, activities , activities
, unit_workdays , unit_workdays
, stuff
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+27
View File
@@ -1656,8 +1656,35 @@ begin
elsif req_path similar to '/(items|projects)' and req_accept = 'text/csv' then elsif req_path similar to '/(items|projects)' and req_accept = 'text/csv' then
perform set_config('response.headers', perform set_config('response.headers',
format('[{"Content-Disposition": "attachment; filename=%s.csv"}]', trim('/' from req_path)), false); format('[{"Content-Disposition": "attachment; filename=%s.csv"}]', trim('/' from req_path)), false);
elsif req_path similar to '/(clients|rpc/getallprojects)' then
perform set_config('response.headers',
'[{"Content-Type": "application/geo+json"}]', false);
else else
perform set_config('response.headers', perform set_config('response.headers',
'[{"X-Custom-Header": "mykey=myval"}]', false); '[{"X-Custom-Header": "mykey=myval"}]', false);
end if; end if;
end; $$ language plpgsql; end; $$ language plpgsql;
create table private.stuff(
id integer primary key
, name text
);
create view test.stuff as select * from private.stuff;
create or replace function location_for_stuff() returns trigger
as $$
begin
insert into private.stuff values (new.id, new.name);
if new.id is not null
then
perform set_config(
'response.headers'
, format('[{"Location": "/%s?id=eq.%s&overriden=true"}]', tg_table_name, new.id)
, false
);
end if;
return new;
end
$$ language plpgsql security definer;
create trigger location_for_stuff instead of insert on test.stuff for each row execute procedure test.location_for_stuff();