Handle overloaded function case
* Add test for params=single-object on GET * Add tests for procs with DEFAULT args * Add tests for overloaded functions * Add test for PATCHing with an empty json array, this previously gave a "Something is wrong" error
This commit is contained in:
committed by
Steve Chávez
parent
38f3bcf4a6
commit
f7e7834a1c
@@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- Computed columns now only work if they belong to the db-schema - @steve-chavez
|
- Computed columns now only work if they belong to the db-schema - @steve-chavez
|
||||||
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
|
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
|
||||||
|
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
|
||||||
|
|
||||||
## [0.4.4.0] - 2018-01-08
|
## [0.4.4.0] - 2018-01-08
|
||||||
|
|
||||||
|
|||||||
+14
-15
@@ -95,8 +95,6 @@ data ApiRequest = ApiRequest {
|
|||||||
, iHeaders :: [(Text, Text)]
|
, iHeaders :: [(Text, Text)]
|
||||||
-- | Request Cookies
|
-- | Request Cookies
|
||||||
, iCookies :: [(Text, Text)]
|
, iCookies :: [(Text, Text)]
|
||||||
-- | Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
|
||||||
, iRpcQParams :: [(Text, Text)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
@@ -116,7 +114,6 @@ userApiRequest schema req reqBody
|
|||||||
, iPreferSingleObjectParameter = singleObject
|
, iPreferSingleObjectParameter = singleObject
|
||||||
, iPreferCount = hasPrefer "count=exact"
|
, iPreferCount = hasPrefer "count=exact"
|
||||||
, iFilters = filters
|
, iFilters = filters
|
||||||
, iRpcQParams = rpcQParams
|
|
||||||
, 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 "*" $ fromMaybe (Just "*") $ 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 ]
|
||||||
@@ -130,6 +127,7 @@ userApiRequest schema req reqBody
|
|||||||
, iCookies = fromMaybe [] $ parseCookiesText <$> lookupHeader "Cookie"
|
, iCookies = fromMaybe [] $ parseCookiesText <$> lookupHeader "Cookie"
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
|
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
||||||
(filters, rpcQParams) =
|
(filters, rpcQParams) =
|
||||||
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
|
||||||
@@ -141,20 +139,22 @@ userApiRequest schema req reqBody
|
|||||||
isEmbedPath = T.isInfixOf "."
|
isEmbedPath = T.isInfixOf "."
|
||||||
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
|
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
|
||||||
payload =
|
payload =
|
||||||
case decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type" of
|
case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of
|
||||||
CTApplicationJSON ->
|
(_, ActionInvoke{isReadOnly=True}) ->
|
||||||
note "All object keys must match" . consPayloadJSON reqBody
|
Right $ PayloadJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
|
||||||
|
(CTApplicationJSON, _) ->
|
||||||
|
note "All object keys must match" . payloadAttributes reqBody
|
||||||
=<< if BL.null reqBody && isTargetingProc
|
=<< if BL.null reqBody && isTargetingProc
|
||||||
then Right emptyObject
|
then Right emptyObject
|
||||||
else JSON.eitherDecode reqBody
|
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" $ consPayloadJSON (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 $ PayloadJSON (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
|
||||||
action =
|
action =
|
||||||
@@ -177,9 +177,8 @@ userApiRequest schema req reqBody
|
|||||||
["rpc", proc] -> TargetProc
|
["rpc", proc] -> TargetProc
|
||||||
$ QualifiedIdentifier schema proc
|
$ QualifiedIdentifier schema proc
|
||||||
other -> TargetUnknown other
|
other -> TargetUnknown other
|
||||||
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}]
|
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}]
|
||||||
relevantPayload | action == ActionInvoke{isReadOnly=True} = Nothing
|
relevantPayload | shouldParsePayload = rightToMaybe payload
|
||||||
| shouldParsePayload = rightToMaybe payload
|
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
path = pathInfo req
|
path = pathInfo req
|
||||||
method = requestMethod req
|
method = requestMethod req
|
||||||
@@ -274,8 +273,8 @@ csvToJson (_, vals) =
|
|||||||
else JSON.String $ toS str
|
else JSON.String $ toS str
|
||||||
)
|
)
|
||||||
|
|
||||||
consPayloadJSON :: BL.ByteString -> JSON.Value -> Maybe PayloadJSON
|
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
|
||||||
consPayloadJSON raw json =
|
payloadAttributes raw json =
|
||||||
-- Test that Array contains only Objects having the same keys
|
-- Test that Array contains only Objects having the same keys
|
||||||
case json of
|
case json of
|
||||||
JSON.Array arr ->
|
JSON.Array arr ->
|
||||||
|
|||||||
+39
-36
@@ -38,7 +38,6 @@ import PostgREST.Config (AppConfig (..))
|
|||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
import PostgREST.DbRequestBuilder( readRequest
|
import PostgREST.DbRequestBuilder( readRequest
|
||||||
, mutateRequest
|
, mutateRequest
|
||||||
, readRpcRequest
|
|
||||||
, fieldNames
|
, fieldNames
|
||||||
)
|
)
|
||||||
import PostgREST.Error ( simpleError, pgError
|
import PostgREST.Error ( simpleError, pgError
|
||||||
@@ -79,35 +78,44 @@ postgrest conf refDbStructure pool worker =
|
|||||||
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest)
|
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest)
|
||||||
|
|
||||||
let authed = containsRole eClaims
|
let authed = containsRole eClaims
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
proc = case (iTarget apiRequest, iPayload apiRequest, iPreferSingleObjectParameter apiRequest) of
|
||||||
txMode = transactionMode dbStructure
|
(TargetProc qi, Just PayloadJSON{pjKeys=pKeys}, s) -> findProc qi pKeys s $ dbProcs dbStructure
|
||||||
(iTarget apiRequest) (iAction apiRequest)
|
_ -> Nothing
|
||||||
|
handleReq = runWithClaims conf eClaims (app dbStructure proc conf) apiRequest
|
||||||
|
txMode = transactionMode proc (iAction apiRequest)
|
||||||
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
|
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
|
||||||
return $ either (pgError authed) identity response
|
return $ either (pgError authed) identity response
|
||||||
when (responseStatus response == status503) worker
|
when (responseStatus response == status503) worker
|
||||||
respond response
|
respond response
|
||||||
|
|
||||||
transactionMode :: DbStructure -> Target -> Action -> H.Mode
|
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> M.HashMap Text [ProcDescription] -> Maybe ProcDescription
|
||||||
transactionMode structure target action =
|
findProc qi payloadKeys paramsAsSingleObject allProcs =
|
||||||
|
let procs = M.lookup (qiName qi) allProcs in
|
||||||
|
-- Handle overloaded functions case
|
||||||
|
join $ (case length <$> procs of
|
||||||
|
Just 1 -> headMay -- if it's not an overloaded function then immediatly get the ProcDescription
|
||||||
|
_ -> find (\x ->
|
||||||
|
if paramsAsSingleObject
|
||||||
|
then length (pdArgs x) == 1 -- if the arg is not of json type let the db give the err
|
||||||
|
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs x))
|
||||||
|
) <$> procs
|
||||||
|
|
||||||
|
transactionMode :: Maybe ProcDescription -> Action -> H.Mode
|
||||||
|
transactionMode proc action =
|
||||||
case action of
|
case action of
|
||||||
ActionRead -> HT.Read
|
ActionRead -> HT.Read
|
||||||
ActionInfo -> HT.Read
|
ActionInfo -> HT.Read
|
||||||
ActionInspect -> HT.Read
|
ActionInspect -> HT.Read
|
||||||
ActionInvoke{isReadOnly=False} ->
|
ActionInvoke{isReadOnly=False} ->
|
||||||
let proc =
|
let v = fromMaybe Volatile $ pdVolatility <$> proc in
|
||||||
case target of
|
|
||||||
(TargetProc qi) -> M.lookup (qiName qi) $
|
|
||||||
dbProcs structure
|
|
||||||
_ -> Nothing
|
|
||||||
v = fromMaybe 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
|
||||||
ActionInvoke{isReadOnly=True} -> HT.Read
|
ActionInvoke{isReadOnly=True} -> HT.Read
|
||||||
_ -> HT.Write
|
_ -> HT.Write
|
||||||
|
|
||||||
app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response
|
app :: DbStructure -> Maybe ProcDescription -> AppConfig -> ApiRequest -> H.Transaction Response
|
||||||
app dbStructure conf apiRequest =
|
app dbStructure proc conf apiRequest =
|
||||||
case responseContentTypeOrError (iAccepts apiRequest) (iAction apiRequest) of
|
case responseContentTypeOrError (iAccepts apiRequest) (iAction apiRequest) of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right contentType ->
|
Right contentType ->
|
||||||
@@ -140,13 +148,13 @@ app dbStructure conf apiRequest =
|
|||||||
case mutateSqlParts of
|
case mutateSqlParts of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right (sq, mq) -> do
|
Right (sq, mq) -> do
|
||||||
let (isSingle, rows) = case pType of
|
let (isSingle, nRows) = case pType of
|
||||||
PJArray len -> (len == 1, len)
|
PJArray len -> (len == 1, len)
|
||||||
PJObject -> (True, 1)
|
PJObject -> (True, 1)
|
||||||
if contentType == CTSingularJSON
|
if contentType == CTSingularJSON
|
||||||
&& not isSingle
|
&& not isSingle
|
||||||
&& iPreferRepresentation apiRequest == Full
|
&& iPreferRepresentation apiRequest == Full
|
||||||
then return $ singularityError (toInteger rows)
|
then return $ singularityError (toInteger nRows)
|
||||||
else do
|
else do
|
||||||
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
|
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
|
||||||
stm = createWriteStatement sq mq
|
stm = createWriteStatement sq mq
|
||||||
@@ -163,7 +171,7 @@ app dbStructure conf apiRequest =
|
|||||||
then Just $ toHeader contentType
|
then Just $ toHeader contentType
|
||||||
else Nothing
|
else Nothing
|
||||||
, Just . contentRangeH 1 0 $
|
, Just . contentRangeH 1 0 $
|
||||||
toInteger <$> if shouldCount then Just rows else Nothing
|
toInteger <$> if shouldCount then Just nRows else Nothing
|
||||||
]
|
]
|
||||||
|
|
||||||
return . responseLBS status201 headers $
|
return . responseLBS status201 headers $
|
||||||
@@ -228,31 +236,28 @@ app dbStructure 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 _isReadOnly, TargetProc qi, payload) ->
|
(ActionInvoke _, TargetProc qi, Just (PayloadJSON payload pType pKeys)) ->
|
||||||
let proc = M.lookup (qiName qi) allProcs
|
let 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
|
rpcBinaryField = if returnsScalar
|
||||||
then Right Nothing
|
then Right Nothing
|
||||||
else binaryField contentType =<< fldNames
|
else binaryField contentType =<< fldNames
|
||||||
parts = (,,) <$> readSqlParts <*> rpcBinaryField <*> rpcQParams in
|
parts = (,) <$> readSqlParts <*> rpcBinaryField in
|
||||||
case parts of
|
case parts of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right ((q, cq), bField, params) -> do
|
Right ((q, cq), bField) -> do
|
||||||
let (prms, keys, isObject) = case payload of
|
let isObject = case pType of
|
||||||
Just (PayloadJSON p (PJArray _) ks) -> (p, ks, False)
|
PJObject -> True
|
||||||
Just (PayloadJSON p PJObject ks) -> (p, ks, True)
|
PJArray _ -> False
|
||||||
Nothing -> (JSON.encode $ M.fromList $ second JSON.toJSON <$> params, S.fromList $ fst <$> params, True)
|
|
||||||
singular = contentType == CTSingularJSON
|
singular = contentType == CTSingularJSON
|
||||||
paramsAsSingleObject = iPreferSingleObjectParameter apiRequest
|
specifiedPgArgs = filter ((`S.member` pKeys) . pgaName) $ fromMaybe [] (pdArgs <$> proc)
|
||||||
specifiedPgArgs = filter (flip S.member keys . pgaName) $ fromMaybe [] (pdArgs <$> proc)
|
row <- H.query (toS payload) $
|
||||||
row <- H.query (toS prms) $
|
|
||||||
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
|
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
|
||||||
singular paramsAsSingleObject
|
singular (iPreferSingleObjectParameter apiRequest)
|
||||||
(contentType == CTTextCSV)
|
(contentType == CTTextCSV)
|
||||||
(contentType == CTOctetStream) _isReadOnly bField
|
(contentType == CTOctetStream) bField isObject
|
||||||
isObject (pgVersion dbStructure)
|
(pgVersion dbStructure)
|
||||||
let (tableTotal, queryTotal, body, jsonHeaders) =
|
let (tableTotal, queryTotal, body, jsonHeaders) =
|
||||||
fromMaybe (Just 0, 0, "[]", "[]") row
|
fromMaybe (Just 0, 0, "[]", "[]") row
|
||||||
(status, contentRange) = rangeHeader queryTotal tableTotal
|
(status, contentRange) = rangeHeader queryTotal tableTotal
|
||||||
@@ -273,7 +278,7 @@ app dbStructure conf apiRequest =
|
|||||||
uri Nothing = ("http", host, port, "/")
|
uri Nothing = ("http", host, port, "/")
|
||||||
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
|
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
|
||||||
uri' = uri proxy
|
uri' = uri proxy
|
||||||
encodeApi ti sd procs = encodeOpenAPI (M.elems procs) (toTableInfo ti) uri' sd (dbPrimaryKeys dbStructure)
|
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd (dbPrimaryKeys dbStructure)
|
||||||
body <- encodeApi <$> H.query schema accessibleTables <*> H.query schema schemaDescription <*> H.query schema accessibleProcs
|
body <- encodeApi <$> H.query schema accessibleTables <*> H.query schema schemaDescription <*> H.query schema accessibleProcs
|
||||||
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
|
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
|
||||||
|
|
||||||
@@ -292,7 +297,6 @@ app dbStructure conf apiRequest =
|
|||||||
filterCol :: Schema -> TableName -> Column -> Bool
|
filterCol :: Schema -> TableName -> Column -> Bool
|
||||||
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
|
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
|
||||||
allPrKeys = dbPrimaryKeys dbStructure
|
allPrKeys = dbPrimaryKeys dbStructure
|
||||||
allProcs = dbProcs dbStructure
|
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
shouldCount = iPreferCount apiRequest
|
shouldCount = iPreferCount apiRequest
|
||||||
schema = toS $ configSchema conf
|
schema = toS $ configSchema conf
|
||||||
@@ -304,11 +308,10 @@ app dbStructure conf apiRequest =
|
|||||||
status = rangeStatus lower upper (toInteger <$> tableTotal)
|
status = rangeStatus lower upper (toInteger <$> tableTotal)
|
||||||
in (status, contentRange)
|
in (status, contentRange)
|
||||||
|
|
||||||
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) allProcs apiRequest
|
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) proc apiRequest
|
||||||
fldNames = fieldNames <$> readReq
|
fldNames = fieldNames <$> readReq
|
||||||
readDbRequest = DbRead <$> readReq
|
readDbRequest = DbRead <$> readReq
|
||||||
mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames)
|
mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames)
|
||||||
rpcQParams = readRpcRequest apiRequest
|
|
||||||
selectQuery = requestToQuery schema False <$> readDbRequest
|
selectQuery = requestToQuery schema False <$> readDbRequest
|
||||||
mutateQuery = requestToQuery schema False <$> mutateDbRequest
|
mutateQuery = requestToQuery schema False <$> mutateDbRequest
|
||||||
countQuery = requestToCountQuery schema <$> readDbRequest
|
countQuery = requestToCountQuery schema <$> readDbRequest
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
module PostgREST.DbRequestBuilder (
|
module PostgREST.DbRequestBuilder (
|
||||||
readRequest
|
readRequest
|
||||||
, mutateRequest
|
, mutateRequest
|
||||||
, readRpcRequest
|
|
||||||
, fieldNames
|
, fieldNames
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -37,8 +36,8 @@ import Protolude hiding (from, dropWhile, drop)
|
|||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
import Unsafe (unsafeHead)
|
import Unsafe (unsafeHead)
|
||||||
|
|
||||||
readRequest :: Maybe Integer -> [Relation] -> M.HashMap Text ProcDescription -> ApiRequest -> Either Response ReadRequest
|
readRequest :: Maybe Integer -> [Relation] -> Maybe ProcDescription -> ApiRequest -> Either Response ReadRequest
|
||||||
readRequest maxRows allRels allProcs apiRequest =
|
readRequest maxRows allRels proc apiRequest =
|
||||||
mapLeft apiRequestError $
|
mapLeft apiRequestError $
|
||||||
treeRestrictRange maxRows =<<
|
treeRestrictRange maxRows =<<
|
||||||
augumentRequestWithJoin schema relations =<<
|
augumentRequestWithJoin schema relations =<<
|
||||||
@@ -48,13 +47,12 @@ readRequest maxRows allRels allProcs apiRequest =
|
|||||||
let target = iTarget apiRequest in
|
let target = iTarget apiRequest in
|
||||||
case target of
|
case target of
|
||||||
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
|
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
|
||||||
(TargetProc (QualifiedIdentifier s proc) ) -> Just (s, tName)
|
(TargetProc (QualifiedIdentifier s pName) ) -> Just (s, tName)
|
||||||
where
|
where
|
||||||
retType = pdReturnType <$> M.lookup proc allProcs
|
tName = case pdReturnType <$> proc of
|
||||||
tName = case retType of
|
|
||||||
Just (SetOf (Composite qi)) -> qiName qi
|
Just (SetOf (Composite qi)) -> qiName qi
|
||||||
Just (Single (Composite qi)) -> qiName qi
|
Just (Single (Composite qi)) -> qiName qi
|
||||||
_ -> proc
|
_ -> pName
|
||||||
|
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
@@ -327,11 +325,6 @@ mutateRequest apiRequest fldNames = mapLeft apiRequestError $
|
|||||||
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
||||||
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
||||||
|
|
||||||
readRpcRequest :: ApiRequest -> Either Response [RpcQParam]
|
|
||||||
readRpcRequest apiRequest = mapLeft apiRequestError rpcQParams
|
|
||||||
where
|
|
||||||
rpcQParams = mapM pRequestRpcQParam $ iRpcQParams apiRequest
|
|
||||||
|
|
||||||
fieldNames :: ReadRequest -> [FieldName]
|
fieldNames :: ReadRequest -> [FieldName]
|
||||||
fieldNames (Node (sel, _) forest) =
|
fieldNames (Node (sel, _) forest) =
|
||||||
map (fst . view _1) (select sel) ++ map colName fks
|
map (fst . view _1) (select sel) ++ map colName fks
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ decodeSynonyms cols =
|
|||||||
<*> HD.value HD.text <*> HD.value HD.text
|
<*> HD.value HD.text <*> HD.value HD.text
|
||||||
<*> HD.value HD.text <*> HD.value HD.text
|
<*> HD.value HD.text <*> HD.value HD.text
|
||||||
|
|
||||||
decodeProcs :: HD.Result (M.HashMap Text ProcDescription)
|
decodeProcs :: HD.Result (M.HashMap Text [ProcDescription])
|
||||||
decodeProcs =
|
decodeProcs =
|
||||||
M.fromList . map addName <$> HD.rowsList tblRow
|
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
|
||||||
|
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowsList tblRow
|
||||||
where
|
where
|
||||||
tblRow = ProcDescription
|
tblRow = ProcDescription
|
||||||
<$> HD.value HD.text
|
<$> HD.value HD.text
|
||||||
@@ -152,10 +153,10 @@ decodeProcs =
|
|||||||
| v == 's' = Stable
|
| v == 's' = Stable
|
||||||
| otherwise = Volatile -- only 'v' can happen here
|
| otherwise = Volatile -- only 'v' can happen here
|
||||||
|
|
||||||
allProcs :: H.Query Schema (M.HashMap Text ProcDescription)
|
allProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
|
||||||
allProcs = H.statement (toS procsSqlQuery) (HE.value HE.text) decodeProcs True
|
allProcs = H.statement (toS procsSqlQuery) (HE.value HE.text) decodeProcs True
|
||||||
|
|
||||||
accessibleProcs :: H.Query Schema (M.HashMap Text ProcDescription)
|
accessibleProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
|
||||||
accessibleProcs = H.statement (toS sql) (HE.value HE.text) decodeProcs True
|
accessibleProcs = H.statement (toS sql) (HE.value HE.text) decodeProcs True
|
||||||
where
|
where
|
||||||
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
||||||
|
|||||||
@@ -47,12 +47,6 @@ pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree
|
|||||||
-- Concat op and v to make pLogicTree argument regular, in the form of "?and=and(.. , ..)" instead of "?and=(.. , ..)"
|
-- Concat op and v to make pLogicTree argument regular, in the form of "?and=and(.. , ..)" instead of "?and=(.. , ..)"
|
||||||
logicTree = join $ parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") . toS <$> ((<>) <$> op <*> pure v)
|
logicTree = join $ parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") . toS <$> ((<>) <$> op <*> pure v)
|
||||||
|
|
||||||
pRequestRpcQParam :: (Text, Text) -> Either ApiRequestError RpcQParam
|
|
||||||
pRequestRpcQParam (k, v) = mapError $ (,) <$> name <*> val
|
|
||||||
where
|
|
||||||
name = parse pFieldName ("failed to parse rpc arg name (" ++ toS k ++ ")") $ toS k
|
|
||||||
val = toS <$> parse (many anyChar) ("failed to parse rpc arg value (" ++ toS v ++ ")") v
|
|
||||||
|
|
||||||
ws :: Parser Text
|
ws :: Parser Text
|
||||||
ws = toS <$> many (oneOf " \t")
|
ws = toS <$> many (oneOf " \t")
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
|
|||||||
|
|
||||||
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
|
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
|
||||||
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
|
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
|
||||||
Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion ->
|
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion ->
|
||||||
H.Query ByteString (Maybe ProcResults)
|
H.Query ByteString (Maybe ProcResults)
|
||||||
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary isReadOnly binaryField isObject pgVer =
|
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField isObject pgVer =
|
||||||
unicodeStatement sql (HE.value HE.unknown) decodeProc True
|
unicodeStatement sql (HE.value HE.unknown) decodeProc True
|
||||||
where
|
where
|
||||||
sql =
|
sql =
|
||||||
@@ -164,15 +164,15 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para
|
|||||||
{responseHeaders} AS response_headers
|
{responseHeaders} AS response_headers
|
||||||
FROM ({selectQuery}) _postgrest_t;|]
|
FROM ({selectQuery}) _postgrest_t;|]
|
||||||
|
|
||||||
(argsRecord, args) | paramsAsSingleObject && not isReadOnly = ("_args_record AS (SELECT NULL)", "$1::json")
|
(argsRecord, args) | paramsAsSingleObject = ("_args_record AS (SELECT NULL)", "$1::json")
|
||||||
| null pgArgs = (ignoredBody, "")
|
| null pgArgs = (ignoredBody, "")
|
||||||
| otherwise = (
|
| otherwise = (
|
||||||
"_args_record AS ( "<>
|
unwords [
|
||||||
"SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <>
|
"_args_record AS (",
|
||||||
"($1) AS _(" <> intercalate ", " ((\a -> pgaName a <> " " <> pgaType a) <$> pgArgs) <> ")" <>
|
"SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <> "($1)",
|
||||||
")"
|
"AS _(" <> intercalate ", " ((\a -> pgaName a <> " " <> pgaType a) <$> pgArgs) <> ")",
|
||||||
, intercalate ", " ((\a -> pgaName a <> " := (SELECT " <> pgaName a <> " FROM _args_record)") <$> pgArgs)
|
")"]
|
||||||
)
|
, intercalate ", " ((\a -> pgaName a <> " := (SELECT " <> pgaName a <> " FROM _args_record)") <$> pgArgs))
|
||||||
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
|
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
|
||||||
_procName = qiName qi
|
_procName = qiName qi
|
||||||
responseHeaders =
|
responseHeaders =
|
||||||
@@ -322,6 +322,7 @@ requestToQuery schema _ (DbMutate (Delete mainTbl logicForest returnings)) =
|
|||||||
-- Due to the use of the `unknown` encoder we need to cast '$1' when the value is not used in the main query
|
-- Due to the use of the `unknown` encoder we need to cast '$1' when the value is not used in the main query
|
||||||
-- otherwise the query will err with a `could not determine data type of parameter $1`.
|
-- otherwise the query will err with a `could not determine data type of parameter $1`.
|
||||||
-- This happens because `unknown` relies on the context to determine the value type.
|
-- This happens because `unknown` relies on the context to determine the value type.
|
||||||
|
-- The error also happens on raw libpq used with C.
|
||||||
ignoredBody :: SqlFragment
|
ignoredBody :: SqlFragment
|
||||||
ignoredBody = "ignored_body AS (SELECT $1::text) "
|
ignoredBody = "ignored_body AS (SELECT $1::text) "
|
||||||
|
|
||||||
|
|||||||
+14
-6
@@ -30,7 +30,8 @@ data DbStructure = DbStructure {
|
|||||||
, dbColumns :: [Column]
|
, dbColumns :: [Column]
|
||||||
, dbRelations :: [Relation]
|
, dbRelations :: [Relation]
|
||||||
, dbPrimaryKeys :: [PrimaryKey]
|
, dbPrimaryKeys :: [PrimaryKey]
|
||||||
, dbProcs :: M.HashMap Text ProcDescription
|
-- ProcDescription is a list because a function can be overloaded
|
||||||
|
, dbProcs :: M.HashMap Text [ProcDescription]
|
||||||
, pgVersion :: PgVersion
|
, pgVersion :: PgVersion
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
@@ -38,14 +39,14 @@ data PgArg = PgArg {
|
|||||||
pgaName :: Text
|
pgaName :: Text
|
||||||
, pgaType :: Text
|
, pgaType :: Text
|
||||||
, pgaReq :: Bool
|
, pgaReq :: Bool
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show)
|
data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
data RetType = Single PgType | SetOf PgType deriving (Eq, Show)
|
data RetType = Single PgType | SetOf PgType deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
data ProcVolatility = Volatile | Stable | Immutable
|
data ProcVolatility = Volatile | Stable | Immutable
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
data ProcDescription = ProcDescription {
|
data ProcDescription = ProcDescription {
|
||||||
pdName :: Text
|
pdName :: Text
|
||||||
@@ -55,6 +56,13 @@ data ProcDescription = ProcDescription {
|
|||||||
, pdVolatility :: ProcVolatility
|
, pdVolatility :: ProcVolatility
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
-- Order by least number of args in the case of overloaded functions
|
||||||
|
instance Ord ProcDescription where
|
||||||
|
ProcDescription name1 des1 args1 rt1 vol1 `compare` ProcDescription name2 des2 args2 rt2 vol2
|
||||||
|
| name1 == name2 && length args1 < length args2 = LT
|
||||||
|
| name1 == name2 && length args1 > length args2 = GT
|
||||||
|
| otherwise = (name1, des1, args1, rt1, vol1) `compare` (name2, des2, args2, rt2, vol2)
|
||||||
|
|
||||||
type Schema = Text
|
type Schema = Text
|
||||||
type TableName = Text
|
type TableName = Text
|
||||||
type SqlQuery = Text
|
type SqlQuery = Text
|
||||||
@@ -111,7 +119,7 @@ data OrderTerm = OrderTerm {
|
|||||||
data QualifiedIdentifier = QualifiedIdentifier {
|
data QualifiedIdentifier = QualifiedIdentifier {
|
||||||
qiSchema :: Schema
|
qiSchema :: Schema
|
||||||
, qiName :: TableName
|
, qiName :: TableName
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
|
||||||
data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
|
data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
|
||||||
|
|||||||
@@ -416,13 +416,21 @@ spec = do
|
|||||||
-- put value back for other tests
|
-- put value back for other tests
|
||||||
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
|
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
|
||||||
|
|
||||||
it "makes no updates and returns 204, when patching with an empty json object" $ do
|
it "makes no updates and returns 204, when patching with an empty json object/array" $ do
|
||||||
request methodPatch "/items" [] [json| {} |]
|
request methodPatch "/items" [] [json| {} |]
|
||||||
`shouldRespondWith` ""
|
`shouldRespondWith` ""
|
||||||
{
|
{
|
||||||
matchStatus = 204,
|
matchStatus = 204,
|
||||||
matchHeaders = ["Content-Range" <:> "*/*"]
|
matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request methodPatch "/items" [] [json| [] |]
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{
|
||||||
|
matchStatus = 204,
|
||||||
|
matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
|
}
|
||||||
|
|
||||||
get "/items" `shouldRespondWith`
|
get "/items" `shouldRespondWith`
|
||||||
[json|[{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15},{id:16},{"id":2},{"id":1}]|]
|
[json|[{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15},{id:16},{"id":2},{"id":1}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
+26
-1
@@ -275,8 +275,14 @@ spec =
|
|||||||
[json|[{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "returns a row result when there are many INOUT params" $
|
it "returns a row result when there are many INOUT params" $
|
||||||
|
get "/rpc/many_inout_params?num=1&str=two&b=false" `shouldRespondWith`
|
||||||
|
[json| [{"num":1,"str":"two","b":false}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can handle procs with args that have a DEFAULT value" $ do
|
||||||
get "/rpc/many_inout_params?num=1&str=two" `shouldRespondWith`
|
get "/rpc/many_inout_params?num=1&str=two" `shouldRespondWith`
|
||||||
[json| [{"num":1,"str":"two","b":true}]|] { matchHeaders = [matchContentTypeJson] }
|
[json| [{"num":1,"str":"two","b":true}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/three_defaults?b=4" `shouldRespondWith`
|
||||||
|
[json|8|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can map a RAISE error code and message to a http status" $
|
it "can map a RAISE error code and message to a http status" $
|
||||||
get "/rpc/raise_pt402"
|
get "/rpc/raise_pt402"
|
||||||
@@ -291,7 +297,7 @@ spec =
|
|||||||
context "expects a single json object" $ do
|
context "expects a single json object" $ do
|
||||||
it "does not expand posted json into parameters" $
|
it "does not expand posted json into parameters" $
|
||||||
request methodPost "/rpc/singlejsonparam"
|
request methodPost "/rpc/singlejsonparam"
|
||||||
[("Prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`
|
[("prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`
|
||||||
[json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]
|
[json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
@@ -304,6 +310,25 @@ spec =
|
|||||||
, "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]
|
, "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works with GET" $
|
||||||
|
request methodGet "/rpc/singlejsonparam?p1=1&p2=text" [("Prefer","params=single-object")] ""
|
||||||
|
`shouldRespondWith` [json|{ "p1": "1", "p2": "text"}|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "should work with an overloaded function" $ do
|
||||||
|
get "/rpc/overloaded" `shouldRespondWith`
|
||||||
|
[json|[{ "overloaded": 1 },
|
||||||
|
{ "overloaded": 2 },
|
||||||
|
{ "overloaded": 3 }]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodPost "/rpc/overloaded" [("Prefer","params=single-object")]
|
||||||
|
[json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]
|
||||||
|
get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [str|"123"|]
|
||||||
|
|
||||||
context "only for POST rpc" $
|
context "only for POST rpc" $
|
||||||
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
|
||||||
|
|||||||
Vendored
+20
-3
@@ -1325,6 +1325,23 @@ $$ language sql;
|
|||||||
create or replace function test.set_cookie_twice() returns void as $$
|
create or replace function test.set_cookie_twice() returns void as $$
|
||||||
set local "response.headers" = '[{"Set-Cookie": "sessionid=38afes7a8; HttpOnly; Path=/"}, {"Set-Cookie": "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"}]';
|
set local "response.headers" = '[{"Set-Cookie": "sessionid=38afes7a8; HttpOnly; Path=/"}, {"Set-Cookie": "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"}]';
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
--
|
|
||||||
-- PostgreSQL database dump complete
|
create or replace function test.three_defaults(a int default 1, b int default 2, c int default 3) returns int as $$
|
||||||
--
|
select a + b + c
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded() returns setof int as $$
|
||||||
|
values (1), (2), (3);
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded(pg_catalog.json) returns table(x int, y text) as $$
|
||||||
|
select * from json_to_recordset($1) as r(x int, y text);
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded(a int, b int) returns int as $$
|
||||||
|
select a + b
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded(a text, b text, c text) returns text as $$
|
||||||
|
select a || b || c
|
||||||
|
$$ language sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user