Fix #690, add columns query arg for RPC/POST/PATCH
* Refactor normalizing json CTE * Refactor CTE to use CASE instead of UNION
This commit is contained in:
committed by
Steve Chávez
parent
3946dfbc64
commit
c9b2830e52
@@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- #690, Add `?columns` query parameter for faster bulk inserts, also ignores unspecified json keys in a payload - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- #1223, Fix incorrect OpenAPI externalDocs url - @steve-chavez
|
- #1223, Fix incorrect OpenAPI externalDocs url - @steve-chavez
|
||||||
|
|||||||
+23
-12
@@ -19,6 +19,7 @@ import qualified Data.ByteString as BS
|
|||||||
import qualified Data.ByteString.Internal as BS (c2w)
|
import qualified Data.ByteString.Internal as BS (c2w)
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Data.Csv as CSV
|
import qualified Data.Csv as CSV
|
||||||
|
import Data.Either.Combinators (mapLeft)
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import Data.List (lookup, last, partition)
|
import Data.List (lookup, last, partition)
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
@@ -32,6 +33,7 @@ import Network.HTTP.Types.Header (hAuthorization, hCookie)
|
|||||||
import Network.HTTP.Types.URI (parseSimpleQuery)
|
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||||
import Network.Wai (Request (..))
|
import Network.Wai (Request (..))
|
||||||
import Network.Wai.Parse (parseHttpAccept)
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
|
import PostgREST.Parsers (pRequestColumns)
|
||||||
import PostgREST.RangeQuery (NonnegRange, rangeRequested, restrictRange, rangeGeq, allRange, rangeLimit, rangeOffset)
|
import PostgREST.RangeQuery (NonnegRange, rangeRequested, restrictRange, rangeGeq, allRange, rangeLimit, rangeOffset)
|
||||||
import Data.Ranged.Boundaries
|
import Data.Ranged.Boundaries
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
@@ -120,7 +122,7 @@ userApiRequest schema req reqBody
|
|||||||
else Nothing
|
else Nothing
|
||||||
, iFilters = filters
|
, iFilters = filters
|
||||||
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
||||||
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
, iSelect = toS $ fromMaybe "*" $ join $ lookup "select" qParams
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||||
, iCanonicalQS = toS $ urlEncodeVars
|
, iCanonicalQS = toS $ urlEncodeVars
|
||||||
. L.sortBy (comparing fst)
|
. L.sortBy (comparing fst)
|
||||||
@@ -137,28 +139,37 @@ userApiRequest schema req reqBody
|
|||||||
case action of
|
case action of
|
||||||
ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
||||||
_ -> (flts, [])
|
_ -> (flts, [])
|
||||||
flts = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn ["order", "limit", "offset", "and", "or"] k) ]
|
flts =
|
||||||
|
[ (toS k, toS $ fromJust v) |
|
||||||
|
(k,v) <- qParams, isJust v,
|
||||||
|
k `notElem` ["select", "columns"],
|
||||||
|
not (endingIn ["order", "limit", "offset", "and", "or"] k) ]
|
||||||
hasOperator val = any (`T.isPrefixOf` val) $
|
hasOperator val = any (`T.isPrefixOf` val) $
|
||||||
((<> ".") <$> "not":M.keys operators) ++
|
((<> ".") <$> "not":M.keys operators) ++
|
||||||
((<> "(") <$> M.keys ftsOperators)
|
((<> "(") <$> M.keys ftsOperators)
|
||||||
isEmbedPath = T.isInfixOf "."
|
isEmbedPath = T.isInfixOf "."
|
||||||
isTargetingProc = (== Just "rpc") $ listToMaybe path
|
isTargetingProc = (== Just "rpc") $ listToMaybe path
|
||||||
|
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
||||||
payload =
|
payload =
|
||||||
case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of
|
case (contentType, action) of
|
||||||
(_, ActionInvoke{isReadOnly=True}) ->
|
(_, ActionInvoke{isReadOnly=True}) ->
|
||||||
Right $ PayloadJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
|
Right $ ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
|
||||||
(CTApplicationJSON, _) ->
|
(CTApplicationJSON, _) ->
|
||||||
note "All object keys must match" . payloadAttributes reqBody
|
let columns | action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}] = toS <$> join (lookup "columns" qParams)
|
||||||
=<< if BL.null reqBody && isTargetingProc
|
| otherwise = Nothing in
|
||||||
then Right emptyObject
|
case columns of
|
||||||
else JSON.eitherDecode reqBody
|
Just cols -> RawJSON reqBody <$> mapLeft show (pRequestColumns cols)
|
||||||
|
Nothing -> note "All object keys must match" . payloadAttributes reqBody
|
||||||
|
=<< if BL.null reqBody && isTargetingProc
|
||||||
|
then Right emptyObject
|
||||||
|
else JSON.eitherDecode reqBody
|
||||||
(CTTextCSV, _) -> do
|
(CTTextCSV, _) -> do
|
||||||
json <- csvToJson <$> CSV.decodeByName reqBody
|
json <- csvToJson <$> CSV.decodeByName reqBody
|
||||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||||
(CTOther "application/x-www-form-urlencoded", _) ->
|
(CTOther "application/x-www-form-urlencoded", _) ->
|
||||||
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
|
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
|
||||||
keys = S.fromList $ M.keys json in
|
keys = S.fromList $ M.keys json in
|
||||||
Right $ PayloadJSON (JSON.encode json) PJObject keys
|
Right $ ProcessedJSON (JSON.encode json) PJObject keys
|
||||||
(ct, _) ->
|
(ct, _) ->
|
||||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges
|
||||||
@@ -291,14 +302,14 @@ payloadAttributes raw json =
|
|||||||
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
|
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
|
||||||
_ -> False) arr in
|
_ -> False) arr in
|
||||||
if areKeysUniform
|
if areKeysUniform
|
||||||
then Just $ PayloadJSON raw (PJArray $ V.length arr) canonicalKeys
|
then Just $ ProcessedJSON raw (PJArray $ V.length arr) canonicalKeys
|
||||||
else Nothing
|
else Nothing
|
||||||
Just _ -> Nothing
|
Just _ -> Nothing
|
||||||
Nothing -> Just emptyPJArray
|
Nothing -> Just emptyPJArray
|
||||||
|
|
||||||
JSON.Object o -> Just $ PayloadJSON raw PJObject (S.fromList $ M.keys o)
|
JSON.Object o -> Just $ ProcessedJSON raw PJObject (S.fromList $ M.keys o)
|
||||||
|
|
||||||
-- truncate everything else to an empty array.
|
-- truncate everything else to an empty array.
|
||||||
_ -> Just emptyPJArray
|
_ -> Just emptyPJArray
|
||||||
where
|
where
|
||||||
emptyPJArray = PayloadJSON (JSON.encode emptyArray) (PJArray 0) S.empty
|
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) (PJArray 0) S.empty
|
||||||
|
|||||||
+10
-11
@@ -78,10 +78,9 @@ postgrest conf refDbStructure pool getTime worker =
|
|||||||
Left err -> return $ apiRequestError err
|
Left err -> return $ apiRequestError err
|
||||||
Right apiRequest -> do
|
Right apiRequest -> do
|
||||||
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf)
|
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf)
|
||||||
|
|
||||||
let authed = containsRole eClaims
|
let authed = containsRole eClaims
|
||||||
proc = case (iTarget apiRequest, iPayload apiRequest, iPreferSingleObjectParameter apiRequest) of
|
proc = case (iTarget apiRequest, iPayload apiRequest, iPreferSingleObjectParameter apiRequest) of
|
||||||
(TargetProc qi, Just PayloadJSON{pjKeys}, s) -> findProc qi pjKeys s $ dbProcs dbStructure
|
(TargetProc qi, Just pJson, s) -> findProc qi (pjKeys pJson) s $ dbProcs dbStructure
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure proc conf) apiRequest
|
handleReq = runWithClaims conf eClaims (app dbStructure proc conf) apiRequest
|
||||||
txMode = transactionMode proc (iAction apiRequest)
|
txMode = transactionMode proc (iAction apiRequest)
|
||||||
@@ -109,7 +108,7 @@ transactionMode proc action =
|
|||||||
ActionInfo -> HT.Read
|
ActionInfo -> HT.Read
|
||||||
ActionInspect -> HT.Read
|
ActionInspect -> HT.Read
|
||||||
ActionInvoke{isReadOnly=False} ->
|
ActionInvoke{isReadOnly=False} ->
|
||||||
let v = maybe Volatile pdVolatility proc in
|
let v = maybe Volatile pdVolatility proc in
|
||||||
if v == Stable || v == Immutable
|
if v == Stable || v == Immutable
|
||||||
then HT.Read
|
then HT.Read
|
||||||
else HT.Write
|
else HT.Write
|
||||||
@@ -146,7 +145,7 @@ app dbStructure proc conf apiRequest =
|
|||||||
)
|
)
|
||||||
] (toS body)
|
] (toS body)
|
||||||
|
|
||||||
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just PayloadJSON{pjRaw}) ->
|
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
||||||
case mutateSqlParts tSchema tName of
|
case mutateSqlParts tSchema tName of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right (sq, mq) -> do
|
Right (sq, mq) -> do
|
||||||
@@ -154,7 +153,7 @@ app dbStructure proc conf apiRequest =
|
|||||||
stm = createWriteStatement sq mq
|
stm = createWriteStatement sq mq
|
||||||
(contentType == CTSingularJSON) True
|
(contentType == CTSingularJSON) True
|
||||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
|
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
|
||||||
row <- H.statement (toS pjRaw) stm
|
row <- H.statement (toS $ pjRaw pJson) stm
|
||||||
let (_, queryTotal, fs, body) = extractQueryResult row
|
let (_, queryTotal, fs, body) = extractQueryResult row
|
||||||
headers = catMaybes [
|
headers = catMaybes [
|
||||||
if null fs
|
if null fs
|
||||||
@@ -180,14 +179,14 @@ app dbStructure proc conf apiRequest =
|
|||||||
if iPreferRepresentation apiRequest == Full
|
if iPreferRepresentation apiRequest == Full
|
||||||
then toS body else ""
|
then toS body else ""
|
||||||
|
|
||||||
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just PayloadJSON{pjRaw}) ->
|
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
||||||
case mutateSqlParts tSchema tName of
|
case mutateSqlParts tSchema tName of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
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) []
|
||||||
row <- H.statement (toS pjRaw) stm
|
row <- H.statement (toS $ pjRaw pJson) stm
|
||||||
let (_, queryTotal, _, body) = extractQueryResult row
|
let (_, queryTotal, _, body) = extractQueryResult row
|
||||||
if contentType == CTSingularJSON
|
if contentType == CTSingularJSON
|
||||||
&& queryTotal /= 1
|
&& queryTotal /= 1
|
||||||
@@ -205,7 +204,7 @@ app dbStructure proc conf apiRequest =
|
|||||||
then responseLBS s [toHeader contentType, r] (toS body)
|
then responseLBS s [toHeader contentType, r] (toS body)
|
||||||
else responseLBS s [r] ""
|
else responseLBS s [r] ""
|
||||||
|
|
||||||
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just PayloadJSON{pjRaw, pjType, pjKeys}) ->
|
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
|
||||||
case mutateSqlParts tSchema tName of
|
case mutateSqlParts tSchema tName of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right (sq, mq) -> do
|
Right (sq, mq) -> do
|
||||||
@@ -267,7 +266,7 @@ app dbStructure proc conf apiRequest =
|
|||||||
let acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in
|
let acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in
|
||||||
return $ responseLBS status200 [allOrigins, acceptH] ""
|
return $ responseLBS status200 [allOrigins, acceptH] ""
|
||||||
|
|
||||||
(ActionInvoke _, TargetProc qi, Just PayloadJSON{pjRaw, pjKeys}) ->
|
(ActionInvoke _, TargetProc qi, Just pJson) ->
|
||||||
let returnsScalar = case proc of
|
let returnsScalar = case proc of
|
||||||
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
|
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
@@ -279,8 +278,8 @@ app dbStructure proc conf apiRequest =
|
|||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right ((q, cq), bField) -> do
|
Right ((q, cq), bField) -> do
|
||||||
let singular = contentType == CTSingularJSON
|
let singular = contentType == CTSingularJSON
|
||||||
specifiedPgArgs = filter ((`S.member` pjKeys) . pgaName) $ maybe [] pdArgs proc
|
specifiedPgArgs = filter ((`S.member` pjKeys pJson) . pgaName) $ maybe [] pdArgs proc
|
||||||
row <- H.statement (toS pjRaw) $
|
row <- H.statement (toS $ pjRaw pJson) $
|
||||||
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
|
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
|
||||||
singular (iPreferSingleObjectParameter apiRequest)
|
singular (iPreferSingleObjectParameter apiRequest)
|
||||||
(contentType == CTTextCSV)
|
(contentType == CTTextCSV)
|
||||||
|
|||||||
@@ -321,8 +321,8 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
|
|||||||
mutateRequest :: ApiRequest -> TableName -> [Text] -> [FieldName] -> Either Response MutateRequest
|
mutateRequest :: ApiRequest -> TableName -> [Text] -> [FieldName] -> Either Response MutateRequest
|
||||||
mutateRequest apiRequest tName pkCols fldNames = mapLeft apiRequestError $
|
mutateRequest apiRequest tName pkCols fldNames = mapLeft apiRequestError $
|
||||||
case action of
|
case action of
|
||||||
ActionCreate -> Right $ Insert tName (pjKeys payload) ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings
|
ActionCreate -> Right $ Insert tName pjCols ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings
|
||||||
ActionUpdate -> Update tName (pjKeys payload) <$> combinedLogic <*> pure returnings
|
ActionUpdate -> Update tName pjCols <$> combinedLogic <*> pure returnings
|
||||||
ActionSingleUpsert ->
|
ActionSingleUpsert ->
|
||||||
(\flts ->
|
(\flts ->
|
||||||
if null (iLogic apiRequest) &&
|
if null (iLogic apiRequest) &&
|
||||||
@@ -331,14 +331,14 @@ mutateRequest apiRequest tName pkCols fldNames = mapLeft apiRequestError $
|
|||||||
all (\case
|
all (\case
|
||||||
Filter _ (OpExpr False (Op "eq" _)) -> True
|
Filter _ (OpExpr False (Op "eq" _)) -> True
|
||||||
_ -> False) flts
|
_ -> False) flts
|
||||||
then Insert tName (pjKeys payload) (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings
|
then Insert tName pjCols (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings
|
||||||
else
|
else
|
||||||
Left InvalidFilters) =<< filters
|
Left InvalidFilters) =<< filters
|
||||||
ActionDelete -> Delete tName <$> combinedLogic <*> pure returnings
|
ActionDelete -> Delete tName <$> combinedLogic <*> pure returnings
|
||||||
_ -> Left UnsupportedVerb
|
_ -> Left UnsupportedVerb
|
||||||
where
|
where
|
||||||
action = iAction apiRequest
|
action = iAction apiRequest
|
||||||
payload = fromJust $ iPayload apiRequest
|
pjCols = pjKeys $ fromJust $ iPayload apiRequest
|
||||||
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames
|
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames
|
||||||
filters = map snd <$> mapM pRequestFilter mutateFilters
|
filters = map snd <$> mapM pRequestFilter mutateFilters
|
||||||
logic = map snd <$> mapM pRequestLogicTree logicFilters
|
logic = map snd <$> mapM pRequestLogicTree logicFilters
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Data.Functor (($>))
|
|||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.Text (intercalate, replace, strip)
|
import Data.Text (intercalate, replace, strip)
|
||||||
import Data.List (init, last)
|
import Data.List (init, last)
|
||||||
|
import qualified Data.Set as S
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
@@ -217,6 +218,13 @@ pLogicPath = do
|
|||||||
notOp = "not." <> op
|
notOp = "not." <> op
|
||||||
return (filter (/= "not") (init path), if "not" `elem` path then notOp else op)
|
return (filter (/= "not") (init path), if "not" `elem` path then notOp else op)
|
||||||
|
|
||||||
|
pRequestColumns :: Text -> Either ParseError (S.Set FieldName)
|
||||||
|
pRequestColumns colStr =
|
||||||
|
S.fromList <$> parse pColumns ("failed to parse columns parameter (" <> toS colStr <> ")") (toS colStr)
|
||||||
|
|
||||||
|
pColumns :: Parser [FieldName]
|
||||||
|
pColumns = pFieldName `sepBy1` lexeme (char ',')
|
||||||
|
|
||||||
mapError :: Either ParseError a -> Either ApiRequestError a
|
mapError :: Either ParseError a -> Either ApiRequestError a
|
||||||
mapError = mapLeft translateError
|
mapError = mapLeft translateError
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -177,13 +177,9 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para
|
|||||||
| null pgArgs = (ignoredBody, "")
|
| null pgArgs = (ignoredBody, "")
|
||||||
| otherwise = (
|
| otherwise = (
|
||||||
unwords [
|
unwords [
|
||||||
"payload AS (SELECT $1::json AS json_data),",
|
normalizedBody <> ",",
|
||||||
"vals AS (",
|
|
||||||
"SELECT json_data AS val FROM payload WHERE json_typeof(json_data) = 'array'",
|
|
||||||
"UNION ALL",
|
|
||||||
"SELECT json_build_array(json_data) AS val FROM payload WHERE json_typeof(json_data) = 'object'),",
|
|
||||||
"_args_record AS (",
|
"_args_record AS (",
|
||||||
"SELECT * FROM json_to_recordset((SELECT val FROM vals)) AS _(" <>
|
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <>
|
||||||
intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")",
|
intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")",
|
||||||
")"]
|
")"]
|
||||||
, intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM _args_record)") <$> pgArgs))
|
, intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM _args_record)") <$> pgArgs))
|
||||||
@@ -276,16 +272,11 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbl tblAlias imp
|
|||||||
getQueryParts _ _ = witness
|
getQueryParts _ _ = witness
|
||||||
requestToQuery schema _ (DbMutate (Insert mainTbl iCols onConflct putConditions returnings)) =
|
requestToQuery schema _ (DbMutate (Insert mainTbl iCols onConflct putConditions returnings)) =
|
||||||
unwords [
|
unwords [
|
||||||
"WITH payload AS (SELECT $1::json AS json_data),",
|
"WITH " <> normalizedBody,
|
||||||
"vals AS (",
|
|
||||||
unwords [
|
|
||||||
"SELECT json_data AS val FROM payload WHERE json_typeof(json_data) = 'array'",
|
|
||||||
"UNION ALL",
|
|
||||||
"SELECT json_build_array(json_data) AS val FROM payload WHERE json_typeof(json_data) = 'object')"],
|
|
||||||
"INSERT INTO ", fromQi qi, if S.null iCols then " " else "(" <> cols <> ")",
|
"INSERT INTO ", fromQi qi, if S.null iCols then " " else "(" <> cols <> ")",
|
||||||
unwords [
|
unwords [
|
||||||
"SELECT " <> cols <> " FROM",
|
"SELECT " <> cols <> " FROM",
|
||||||
"json_populate_recordset", "(null::", fromQi qi, ", (SELECT val FROM vals)) _",
|
"json_populate_recordset", "(null::", fromQi qi, ", " <> selectBody <> ") _",
|
||||||
-- Only used for PUT
|
-- Only used for PUT
|
||||||
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> putConditions)) `emptyOnFalse` null putConditions],
|
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> putConditions)) `emptyOnFalse` null putConditions],
|
||||||
maybe "" (\(oncDo, oncCols) -> (
|
maybe "" (\(oncDo, oncCols) -> (
|
||||||
@@ -304,13 +295,9 @@ requestToQuery schema _ (DbMutate (Update mainTbl uCols logicForest returnings))
|
|||||||
then "WITH " <> ignoredBody <> "SELECT null WHERE false" -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
then "WITH " <> ignoredBody <> "SELECT null WHERE false" -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
||||||
else
|
else
|
||||||
unwords [
|
unwords [
|
||||||
"WITH payload AS (SELECT $1::json AS json_data),",
|
"WITH " <> normalizedBody,
|
||||||
"vals AS (",
|
|
||||||
"SELECT json_data AS val FROM payload WHERE json_typeof(json_data) = 'array'",
|
|
||||||
"UNION ALL",
|
|
||||||
"SELECT json_build_array(json_data) AS val FROM payload WHERE json_typeof(json_data) = 'object')",
|
|
||||||
"UPDATE " <> fromQi qi <> " SET " <> cols,
|
"UPDATE " <> fromQi qi <> " SET " <> cols,
|
||||||
"FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi qi, ", (SELECT val FROM vals))) _ ",
|
"FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi qi, ", " <> selectBody <> ")) _ ",
|
||||||
("WHERE " <> intercalate " AND " (pgFmtLogicTree qi <$> logicForest)) `emptyOnFalse` null logicForest,
|
("WHERE " <> intercalate " AND " (pgFmtLogicTree qi <$> logicForest)) `emptyOnFalse` null logicForest,
|
||||||
("RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings)) `emptyOnFalse` null returnings
|
("RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings)) `emptyOnFalse` null returnings
|
||||||
]
|
]
|
||||||
@@ -334,6 +321,25 @@ requestToQuery schema _ (DbMutate (Delete mainTbl logicForest returnings)) =
|
|||||||
ignoredBody :: SqlFragment
|
ignoredBody :: SqlFragment
|
||||||
ignoredBody = "ignored_body AS (SELECT $1::text) "
|
ignoredBody = "ignored_body AS (SELECT $1::text) "
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
|
||||||
|
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
|
||||||
|
-- We do this in SQL to avoid processing the JSON in application code
|
||||||
|
normalizedBody :: SqlFragment
|
||||||
|
normalizedBody =
|
||||||
|
unwords [
|
||||||
|
"pgrst_payload AS (SELECT $1::json AS json_data),",
|
||||||
|
"pgrst_body AS (",
|
||||||
|
"SELECT",
|
||||||
|
"CASE WHEN json_typeof(json_data) = 'array'",
|
||||||
|
"THEN json_data",
|
||||||
|
"ELSE json_build_array(json_data)",
|
||||||
|
"END AS val",
|
||||||
|
"FROM pgrst_payload)"]
|
||||||
|
|
||||||
|
selectBody :: SqlFragment
|
||||||
|
selectBody = "(SELECT val FROM pgrst_body)"
|
||||||
|
|
||||||
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
|
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
|
||||||
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl
|
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl
|
||||||
|
|
||||||
|
|||||||
+15
-10
@@ -187,16 +187,21 @@ data Relation = Relation {
|
|||||||
isSelfJoin :: Relation -> Bool
|
isSelfJoin :: Relation -> Bool
|
||||||
isSelfJoin r = relType r /= Root && relTable r == relFTable r
|
isSelfJoin r = relType r /= Root && relTable r == relFTable r
|
||||||
|
|
||||||
-- | Cached attributes of a JSON payload
|
data PayloadJSON =
|
||||||
data PayloadJSON = PayloadJSON {
|
-- | Cached attributes of a JSON payload
|
||||||
-- | This is the raw ByteString that comes from the request body.
|
ProcessedJSON {
|
||||||
-- We cache this instead of an Aeson Value because it was detected that for large payloads the encoding
|
-- | This is the raw ByteString that comes from the request body.
|
||||||
-- had high memory usage, see #1005 for more details
|
-- We cache this instead of an Aeson Value because it was detected that for large payloads the encoding
|
||||||
pjRaw :: BL.ByteString
|
-- had high memory usage, see #1005 for more details
|
||||||
, pjType :: PJType
|
pjRaw :: BL.ByteString
|
||||||
-- | Keys of the object or if it's an array these keys are guaranteed to be the same across all its objects
|
, pjType :: PJType
|
||||||
, pjKeys :: S.Set Text
|
-- | Keys of the object or if it's an array these keys are guaranteed to be the same across all its objects
|
||||||
} deriving (Show, Eq)
|
, pjKeys :: S.Set Text
|
||||||
|
}|
|
||||||
|
RawJSON {
|
||||||
|
pjRaw :: BL.ByteString
|
||||||
|
, pjKeys :: S.Set Text
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
data PJType = PJArray { pjaLength :: Int } | PJObject deriving (Show, Eq)
|
data PJType = PJArray { pjaLength :: Int } | PJObject deriving (Show, Eq)
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,48 @@ spec = do
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "POST with ?columns parameter" $ do
|
||||||
|
it "ignores json keys not included in ?columns" $ do
|
||||||
|
request methodPost "/articles?columns=id,body" [("Prefer", "return=representation")]
|
||||||
|
[json| {"id": 200, "body": "xxx", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith`
|
||||||
|
[json|[{"id": 200, "body": "xxx", "owner": "postgrest_test_anonymous"}]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = [] }
|
||||||
|
request methodPost "/articles?columns=id,body&select=id,body" [("Prefer", "return=representation")]
|
||||||
|
[json| [
|
||||||
|
{"id": 201, "body": "yyy", "smth": "here", "other": "stuff", "fake_id": 13},
|
||||||
|
{"id": 202, "body": "zzz", "garbage": "%%$&", "kkk": "jjj"},
|
||||||
|
{"id": 203, "body": "aaa", "hey": "ho"} ]|] `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id": 201, "body": "yyy"},
|
||||||
|
{"id": 202, "body": "zzz"},
|
||||||
|
{"id": 203, "body": "aaa"} ]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = [] }
|
||||||
|
|
||||||
|
-- TODO parse columns error message needs to be improved
|
||||||
|
it "disallows blank ?columns" $
|
||||||
|
post "/articles?columns="
|
||||||
|
[json|[
|
||||||
|
{"id": 204, "body": "yyy"},
|
||||||
|
{"id": 205, "body": "zzz"}]|] `shouldRespondWith` 400
|
||||||
|
|
||||||
|
it "disallows array elements that are not json objects" $
|
||||||
|
post "/articles?columns=id,body"
|
||||||
|
[json|[
|
||||||
|
{"id": 204, "body": "yyy"},
|
||||||
|
333,
|
||||||
|
"asdf",
|
||||||
|
{"id": 205, "body": "zzz"}]|] `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code": "22023",
|
||||||
|
"details": null,
|
||||||
|
"hint": null,
|
||||||
|
"message": "argument of json_populate_recordset must be an array of objects"}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
describe "CSV insert" $ do
|
describe "CSV insert" $ do
|
||||||
|
|
||||||
context "disparate csv types" $
|
context "disparate csv types" $
|
||||||
@@ -464,6 +506,14 @@ spec = do
|
|||||||
simpleBody p `shouldBe` "["<>payload<>"]"
|
simpleBody p `shouldBe` "["<>payload<>"]"
|
||||||
simpleStatus p `shouldBe` ok200
|
simpleStatus p `shouldBe` ok200
|
||||||
|
|
||||||
|
context "PATCH with ?columns parameter" $
|
||||||
|
it "ignores json keys not included in ?columns" $
|
||||||
|
request methodPatch "/articles?id=eq.200&columns=body" [("Prefer", "return=representation")]
|
||||||
|
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith`
|
||||||
|
[json|[{"id": 200, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [] }
|
||||||
|
|
||||||
describe "Row level permission" $
|
describe "Row level permission" $
|
||||||
it "set user_id when inserting rows" $ do
|
it "set user_id when inserting rows" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
|
||||||
|
|||||||
@@ -339,10 +339,16 @@ spec =
|
|||||||
get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]
|
get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]
|
||||||
get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [str|"123"|]
|
get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [str|"123"|]
|
||||||
|
|
||||||
context "only for POST rpc" $
|
context "only for POST rpc" $ do
|
||||||
it "gives a parse filter error if GET style proc args are specified" $
|
it "gives a parse filter error if GET style proc args are specified" $
|
||||||
post "/rpc/sayhello?name=John" [json|{}|] `shouldRespondWith` 400
|
post "/rpc/sayhello?name=John" [json|{}|] `shouldRespondWith` 400
|
||||||
|
|
||||||
|
it "ignores json keys not included in ?columns" $
|
||||||
|
post "/rpc/sayhello?columns=name"
|
||||||
|
[json|{"name": "John", "smth": "here", "other": "stuff", "fake_id": 13}|] `shouldRespondWith`
|
||||||
|
[json|"Hello, John"|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "only for GET rpc" $ do
|
context "only for GET rpc" $ do
|
||||||
it "should fail on mutating procs" $ do
|
it "should fail on mutating procs" $ do
|
||||||
get "/rpc/callcounter" `shouldRespondWith` 500
|
get "/rpc/callcounter" `shouldRespondWith` 500
|
||||||
|
|||||||
+12
-12
@@ -96,21 +96,21 @@ setUp
|
|||||||
|
|
||||||
echo "Running memory usage tests.."
|
echo "Running memory usage tests.."
|
||||||
|
|
||||||
jsonKeyTest "1M" "POST" "/rpc/leak" "20M"
|
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "12M"
|
||||||
jsonKeyTest "1M" "POST" "/leak" "20M"
|
jsonKeyTest "1M" "POST" "/leak?columns=blob" "12M"
|
||||||
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1" "20M"
|
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "12M"
|
||||||
|
|
||||||
jsonKeyTest "10M" "POST" "/rpc/leak" "105M"
|
jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "40M"
|
||||||
jsonKeyTest "10M" "POST" "/leak" "105M"
|
jsonKeyTest "10M" "POST" "/leak?columns=blob" "40M"
|
||||||
jsonKeyTest "10M" "PATCH" "/leak?id=eq.1" "105M"
|
jsonKeyTest "10M" "PATCH" "/leak?id=eq.1&columns=blob" "40M"
|
||||||
|
|
||||||
jsonKeyTest "50M" "POST" "/rpc/leak" "500M"
|
jsonKeyTest "50M" "POST" "/rpc/leak?columns=blob" "170M"
|
||||||
jsonKeyTest "50M" "POST" "/leak" "500M"
|
jsonKeyTest "50M" "POST" "/leak?columns=blob" "170M"
|
||||||
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1" "500M"
|
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "170M"
|
||||||
|
|
||||||
postJsonArrayTest "1000" "/perf_articles" "20M"
|
postJsonArrayTest "1000" "/perf_articles?columns=id,body" "9M"
|
||||||
postJsonArrayTest "10000" "/perf_articles" "150M"
|
postJsonArrayTest "10000" "/perf_articles?columns=id,body" "10M"
|
||||||
postJsonArrayTest "100000" "/perf_articles" "1.15G"
|
postJsonArrayTest "100000" "/perf_articles?columns=id,body" "20M"
|
||||||
|
|
||||||
cleanUp
|
cleanUp
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user