Add binary output to rpc, Fix #883
This commit is contained in:
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- #889, Allow more than two conditions in a single and/or - @steve-chavez
|
- #889, Allow more than two conditions in a single and/or - @steve-chavez
|
||||||
|
- #883, Binary output support for RPC - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+16
-10
@@ -232,19 +232,25 @@ app dbStructure conf apiRequest =
|
|||||||
return $ responseLBS status200 [allOrigins, acceptH] ""
|
return $ responseLBS status200 [allOrigins, acceptH] ""
|
||||||
|
|
||||||
(ActionInvoke, TargetProc qi, Just (PayloadJSON payload)) ->
|
(ActionInvoke, TargetProc qi, Just (PayloadJSON payload)) ->
|
||||||
case readSqlParts of
|
let proc = M.lookup (qiName qi) allProcs
|
||||||
Left errorResponse -> return errorResponse
|
|
||||||
Right (q, cq) -> do
|
|
||||||
let p = V.head payload
|
|
||||||
singular = contentType == CTSingularJSON
|
|
||||||
paramsAsSingleObject = iPreferSingleObjectParameter apiRequest
|
|
||||||
proc = M.lookup (qiName qi) allProcs
|
|
||||||
returnsScalar = case proc of
|
returnsScalar = case proc of
|
||||||
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
|
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
rpcBinaryField = if returnsScalar
|
||||||
|
then Right Nothing
|
||||||
|
else binaryField contentType =<< fldNames
|
||||||
|
partsField = (,) <$> readSqlParts <*> rpcBinaryField in
|
||||||
|
case partsField of
|
||||||
|
Left errorResponse -> return errorResponse
|
||||||
|
Right ((q, cq), bField) -> do
|
||||||
|
let p = V.head payload
|
||||||
|
singular = contentType == CTSingularJSON
|
||||||
|
paramsAsSingleObject = iPreferSingleObjectParameter apiRequest
|
||||||
row <- H.query () $
|
row <- H.query () $
|
||||||
callProc qi p returnsScalar q cq topLevelRange shouldCount singular
|
callProc qi p returnsScalar q cq topLevelRange shouldCount
|
||||||
paramsAsSingleObject (contentType == CTTextCSV)
|
singular paramsAsSingleObject
|
||||||
|
(contentType == CTTextCSV)
|
||||||
|
(contentType == CTOctetStream) bField
|
||||||
let (tableTotal, queryTotal, body) =
|
let (tableTotal, queryTotal, body) =
|
||||||
fromMaybe (Just 0, 0, "[]") row
|
fromMaybe (Just 0, 0, "[]") row
|
||||||
(status, contentRange) = rangeHeader queryTotal tableTotal
|
(status, contentRange) = rangeHeader queryTotal tableTotal
|
||||||
@@ -311,7 +317,7 @@ responseContentTypeOrError accepts action = serves contentTypesForRequest accept
|
|||||||
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionInvoke -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionInvoke -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream]
|
||||||
ActionInspect -> [CTOpenAPI, CTApplicationJSON]
|
ActionInspect -> [CTOpenAPI, CTApplicationJSON]
|
||||||
ActionInfo -> [CTTextCSV]
|
ActionInfo -> [CTTextCSV]
|
||||||
serves sProduces cAccepts =
|
serves sProduces cAccepts =
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
|
|||||||
|
|
||||||
type ProcResults = (Maybe Int64, Int64, ByteString)
|
type ProcResults = (Maybe Int64, Int64, ByteString)
|
||||||
callProc :: QualifiedIdentifier -> JSON.Object -> Bool -> SqlQuery -> SqlQuery -> NonnegRange ->
|
callProc :: QualifiedIdentifier -> JSON.Object -> Bool -> SqlQuery -> SqlQuery -> NonnegRange ->
|
||||||
Bool -> Bool -> Bool -> Bool -> H.Query () (Maybe ProcResults)
|
Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> H.Query () (Maybe ProcResults)
|
||||||
callProc qi params returnsScalar selectQuery countQuery _ countTotal isSingle paramsAsJson asCsv =
|
callProc qi params returnsScalar selectQuery countQuery _ countTotal isSingle paramsAsJson asCsv asBinary binaryField =
|
||||||
unicodeStatement sql HE.unit decodeProc True
|
unicodeStatement sql HE.unit decodeProc True
|
||||||
where
|
where
|
||||||
sql =
|
sql =
|
||||||
@@ -155,7 +155,7 @@ callProc qi params returnsScalar selectQuery countQuery _ countTotal isSingle pa
|
|||||||
SELECT
|
SELECT
|
||||||
{countResultF} AS total_result_set,
|
{countResultF} AS total_result_set,
|
||||||
1 AS page_total,
|
1 AS page_total,
|
||||||
(row_to_json(_postgrest_t)->{_procName})::character varying as body
|
{scalarBodyF} as body
|
||||||
FROM ({selectQuery}) _postgrest_t;|]
|
FROM ({selectQuery}) _postgrest_t;|]
|
||||||
else [qc|
|
else [qc|
|
||||||
WITH {sourceCTEName} AS ({_callSql})
|
WITH {sourceCTEName} AS ({_callSql})
|
||||||
@@ -165,22 +165,24 @@ callProc qi params returnsScalar selectQuery countQuery _ countTotal isSingle pa
|
|||||||
{bodyF} as body
|
{bodyF} as body
|
||||||
FROM ({selectQuery}) _postgrest_t;|]
|
FROM ({selectQuery}) _postgrest_t;|]
|
||||||
|
|
||||||
countResultF = if countTotal then "("<>countQuery<>")" else "null::bigint" :: Text
|
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
|
||||||
_args = if paramsAsJson
|
_args = if paramsAsJson
|
||||||
then insertableValueWithType "json" $ JSON.Object params
|
then insertableValueWithType "json" $ JSON.Object params
|
||||||
else intercalate "," $ map _assignment (HM.toList params)
|
else intercalate "," $ map _assignment (HM.toList params)
|
||||||
_procName = pgFmtLit $ qiName qi
|
_procName = qiName qi
|
||||||
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
|
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
|
||||||
_callSql = [qc|select * from {fromQi qi}({_args}) |] :: Text
|
_callSql = [qc|select * from {fromQi qi}({_args}) |] :: Text
|
||||||
_countExpr = if countTotal
|
|
||||||
then [qc|(select pg_catalog.count(*) from {sourceCTEName})|]
|
|
||||||
else "null::bigint" :: Text
|
|
||||||
decodeProc = HD.maybeRow procRow
|
decodeProc = HD.maybeRow procRow
|
||||||
procRow = (,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
|
procRow = (,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
|
||||||
<*> HD.value HD.bytea
|
<*> HD.value HD.bytea
|
||||||
|
scalarBodyF
|
||||||
|
| asBinary = asBinaryF _procName
|
||||||
|
| otherwise = "(row_to_json(_postgrest_t)->" <> pgFmtLit _procName <> ")::character varying"
|
||||||
|
|
||||||
bodyF
|
bodyF
|
||||||
| isSingle = asJsonSingleF
|
| isSingle = asJsonSingleF
|
||||||
| asCsv = asCsvF
|
| asCsv = asCsvF
|
||||||
|
| isJust binaryField = asBinaryF $ fromJust binaryField
|
||||||
| otherwise = asJsonF
|
| otherwise = asJsonF
|
||||||
|
|
||||||
pgFmtIdent :: SqlFragment -> SqlFragment
|
pgFmtIdent :: SqlFragment -> SqlFragment
|
||||||
|
|||||||
@@ -693,6 +693,7 @@ spec = do
|
|||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
describe "binary output" $ do
|
describe "binary output" $ do
|
||||||
|
context "on GET" $ do
|
||||||
it "can query if a single column is selected" $
|
it "can query if a single column is selected" $
|
||||||
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
||||||
@@ -714,6 +715,28 @@ spec = do
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "on RPC" $ do
|
||||||
|
context "Proc that returns scalar" $
|
||||||
|
it "can query without selecting column" $
|
||||||
|
request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
context "Proc that returns rows" $ do
|
||||||
|
it "can query if a single column is selected" $
|
||||||
|
request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "fails if a single column is not selected" $
|
||||||
|
request methodPost "/rpc/ret_rows_with_base64_bin" (acceptHdrs "application/octet-stream") ""
|
||||||
|
`shouldRespondWith` 406
|
||||||
|
|
||||||
describe "HTTP request env vars" $ do
|
describe "HTTP request env vars" $ do
|
||||||
it "custom header is set" $
|
it "custom header is set" $
|
||||||
request methodPost "/rpc/get_guc_value"
|
request methodPost "/rpc/get_guc_value"
|
||||||
|
|||||||
Vendored
+9
@@ -1115,6 +1115,7 @@ create table images (
|
|||||||
);
|
);
|
||||||
|
|
||||||
create view images_base64 as (
|
create view images_base64 as (
|
||||||
|
-- encoding in base64 puts a '\n' after every 76 character due to legacy reasons, this is isn't necessary here so it's removed
|
||||||
select name, replace(encode(img, 'base64'), E'\n', '') as img from images
|
select name, replace(encode(img, 'base64'), E'\n', '') as img from images
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1161,6 +1162,14 @@ $$ language sql;
|
|||||||
|
|
||||||
create function test.ret_void() returns void as '' language sql;
|
create function test.ret_void() returns void as '' language sql;
|
||||||
|
|
||||||
|
create function test.ret_base64_bin() returns text as $$
|
||||||
|
select i.img from test.images_base64 i where i.name = 'A.png';
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.ret_rows_with_base64_bin() returns setof test.images_base64 as $$
|
||||||
|
select i.name, i.img from test.images_base64 i;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
create function test.single_article(id integer) returns test.articles as $$
|
create function test.single_article(id integer) returns test.articles as $$
|
||||||
select a.* from test.articles a where a.id = $1;
|
select a.* from test.articles a where a.id = $1;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user