Allow overriding headers Location,Content-Type,etc
This commit is contained in:
committed by
Steve Chavez
parent
c7f78fa7fc
commit
9b1224827a
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
Vendored
+1
@@ -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;
|
||||||
|
|||||||
Vendored
+27
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user