Add response.headers on GET/POST/PATCH/PUT/DELETE
This commit is contained in:
committed by
Steve Chavez
parent
7dade7f466
commit
c7f78fa7fc
+3
-2
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Vendored
+19
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user