diff --git a/CHANGELOG.md b/CHANGELOG.md index eba086696..911b009f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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 diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index 1f8b3ac3e..cec3bfdda 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -95,8 +95,6 @@ data ApiRequest = ApiRequest { , iHeaders :: [(Text, Text)] -- | Request Cookies , 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. @@ -116,7 +114,6 @@ userApiRequest schema req reqBody , iPreferSingleObjectParameter = singleObject , iPreferCount = hasPrefer "count=exact" , iFilters = filters - , iRpcQParams = rpcQParams , iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ] , iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams , 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" } where + -- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..) (filters, rpcQParams) = case action of ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts @@ -141,20 +139,22 @@ userApiRequest schema req reqBody isEmbedPath = T.isInfixOf "." isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path payload = - case decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type" of - CTApplicationJSON -> - note "All object keys must match" . consPayloadJSON reqBody + case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of + (_, ActionInvoke{isReadOnly=True}) -> + 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 then Right emptyObject else JSON.eitherDecode reqBody - CTTextCSV -> do + (CTTextCSV, _) -> do json <- csvToJson <$> CSV.decodeByName reqBody - note "All lines must have same number of fields" $ consPayloadJSON (JSON.encode json) json - CTOther "application/x-www-form-urlencoded" -> + note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json + (CTOther "application/x-www-form-urlencoded", _) -> let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody keys = S.fromList $ M.keys json in Right $ PayloadJSON (JSON.encode json) PJObject keys - ct -> + (ct, _) -> Left $ toS $ "Content-Type not acceptable: " <> toMime ct topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges action = @@ -177,9 +177,8 @@ userApiRequest schema req reqBody ["rpc", proc] -> TargetProc $ QualifiedIdentifier schema proc other -> TargetUnknown other - shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}] - relevantPayload | action == ActionInvoke{isReadOnly=True} = Nothing - | shouldParsePayload = rightToMaybe payload + shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}] + relevantPayload | shouldParsePayload = rightToMaybe payload | otherwise = Nothing path = pathInfo req method = requestMethod req @@ -274,8 +273,8 @@ csvToJson (_, vals) = else JSON.String $ toS str ) -consPayloadJSON :: BL.ByteString -> JSON.Value -> Maybe PayloadJSON -consPayloadJSON raw json = +payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON +payloadAttributes raw json = -- Test that Array contains only Objects having the same keys case json of JSON.Array arr -> diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index ea449c5a1..2733a37c1 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -38,7 +38,6 @@ import PostgREST.Config (AppConfig (..)) import PostgREST.DbStructure import PostgREST.DbRequestBuilder( readRequest , mutateRequest - , readRpcRequest , fieldNames ) import PostgREST.Error ( simpleError, pgError @@ -79,35 +78,44 @@ postgrest conf refDbStructure pool worker = eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) let authed = containsRole eClaims - handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest - txMode = transactionMode dbStructure - (iTarget apiRequest) (iAction apiRequest) + proc = case (iTarget apiRequest, iPayload apiRequest, iPreferSingleObjectParameter apiRequest) of + (TargetProc qi, Just PayloadJSON{pjKeys=pKeys}, s) -> findProc qi pKeys s $ dbProcs dbStructure + _ -> 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 return $ either (pgError authed) identity response when (responseStatus response == status503) worker respond response -transactionMode :: DbStructure -> Target -> Action -> H.Mode -transactionMode structure target action = +findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> M.HashMap Text [ProcDescription] -> Maybe ProcDescription +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 ActionRead -> HT.Read ActionInfo -> HT.Read ActionInspect -> HT.Read ActionInvoke{isReadOnly=False} -> - let proc = - case target of - (TargetProc qi) -> M.lookup (qiName qi) $ - dbProcs structure - _ -> Nothing - v = fromMaybe Volatile $ pdVolatility <$> proc in + let v = fromMaybe Volatile $ pdVolatility <$> proc in if v == Stable || v == Immutable then HT.Read else HT.Write ActionInvoke{isReadOnly=True} -> HT.Read _ -> HT.Write -app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response -app dbStructure conf apiRequest = +app :: DbStructure -> Maybe ProcDescription -> AppConfig -> ApiRequest -> H.Transaction Response +app dbStructure proc conf apiRequest = case responseContentTypeOrError (iAccepts apiRequest) (iAction apiRequest) of Left errorResponse -> return errorResponse Right contentType -> @@ -140,13 +148,13 @@ app dbStructure conf apiRequest = case mutateSqlParts of Left errorResponse -> return errorResponse Right (sq, mq) -> do - let (isSingle, rows) = case pType of - PJArray len -> (len == 1, len) - PJObject -> (True, 1) + let (isSingle, nRows) = case pType of + PJArray len -> (len == 1, len) + PJObject -> (True, 1) if contentType == CTSingularJSON && not isSingle && iPreferRepresentation apiRequest == Full - then return $ singularityError (toInteger rows) + then return $ singularityError (toInteger nRows) else do 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 @@ -163,7 +171,7 @@ app dbStructure conf apiRequest = then Just $ toHeader contentType else Nothing , Just . contentRangeH 1 0 $ - toInteger <$> if shouldCount then Just rows else Nothing + toInteger <$> if shouldCount then Just nRows else Nothing ] 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 return $ responseLBS status200 [allOrigins, acceptH] "" - (ActionInvoke _isReadOnly, TargetProc qi, payload) -> - let proc = M.lookup (qiName qi) allProcs - returnsScalar = case proc of + (ActionInvoke _, TargetProc qi, Just (PayloadJSON payload pType pKeys)) -> + let returnsScalar = case proc of Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True _ -> False rpcBinaryField = if returnsScalar then Right Nothing else binaryField contentType =<< fldNames - parts = (,,) <$> readSqlParts <*> rpcBinaryField <*> rpcQParams in + parts = (,) <$> readSqlParts <*> rpcBinaryField in case parts of Left errorResponse -> return errorResponse - Right ((q, cq), bField, params) -> do - let (prms, keys, isObject) = case payload of - Just (PayloadJSON p (PJArray _) ks) -> (p, ks, False) - Just (PayloadJSON p PJObject ks) -> (p, ks, True) - Nothing -> (JSON.encode $ M.fromList $ second JSON.toJSON <$> params, S.fromList $ fst <$> params, True) + Right ((q, cq), bField) -> do + let isObject = case pType of + PJObject -> True + PJArray _ -> False singular = contentType == CTSingularJSON - paramsAsSingleObject = iPreferSingleObjectParameter apiRequest - specifiedPgArgs = filter (flip S.member keys . pgaName) $ fromMaybe [] (pdArgs <$> proc) - row <- H.query (toS prms) $ + specifiedPgArgs = filter ((`S.member` pKeys) . pgaName) $ fromMaybe [] (pdArgs <$> proc) + row <- H.query (toS payload) $ callProc qi specifiedPgArgs returnsScalar q cq shouldCount - singular paramsAsSingleObject + singular (iPreferSingleObjectParameter apiRequest) (contentType == CTTextCSV) - (contentType == CTOctetStream) _isReadOnly bField - isObject (pgVersion dbStructure) + (contentType == CTOctetStream) bField isObject + (pgVersion dbStructure) let (tableTotal, queryTotal, body, jsonHeaders) = fromMaybe (Just 0, 0, "[]", "[]") row (status, contentRange) = rangeHeader queryTotal tableTotal @@ -273,7 +278,7 @@ app dbStructure conf apiRequest = uri Nothing = ("http", host, port, "/") uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b) 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 return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body @@ -292,7 +297,6 @@ app dbStructure conf apiRequest = filterCol :: Schema -> TableName -> Column -> Bool filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb allPrKeys = dbPrimaryKeys dbStructure - allProcs = dbProcs dbStructure allOrigins = ("Access-Control-Allow-Origin", "*") :: Header shouldCount = iPreferCount apiRequest schema = toS $ configSchema conf @@ -304,11 +308,10 @@ app dbStructure conf apiRequest = status = rangeStatus lower upper (toInteger <$> tableTotal) in (status, contentRange) - readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) allProcs apiRequest + readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) proc apiRequest fldNames = fieldNames <$> readReq readDbRequest = DbRead <$> readReq mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames) - rpcQParams = readRpcRequest apiRequest selectQuery = requestToQuery schema False <$> readDbRequest mutateQuery = requestToQuery schema False <$> mutateDbRequest countQuery = requestToCountQuery schema <$> readDbRequest diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index 601d70957..be1a53c05 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -3,7 +3,6 @@ module PostgREST.DbRequestBuilder ( readRequest , mutateRequest -, readRpcRequest , fieldNames ) where @@ -37,8 +36,8 @@ import Protolude hiding (from, dropWhile, drop) import Text.Regex.TDFA ((=~)) import Unsafe (unsafeHead) -readRequest :: Maybe Integer -> [Relation] -> M.HashMap Text ProcDescription -> ApiRequest -> Either Response ReadRequest -readRequest maxRows allRels allProcs apiRequest = +readRequest :: Maybe Integer -> [Relation] -> Maybe ProcDescription -> ApiRequest -> Either Response ReadRequest +readRequest maxRows allRels proc apiRequest = mapLeft apiRequestError $ treeRestrictRange maxRows =<< augumentRequestWithJoin schema relations =<< @@ -48,13 +47,12 @@ readRequest maxRows allRels allProcs apiRequest = let target = iTarget apiRequest in case target of (TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t) - (TargetProc (QualifiedIdentifier s proc) ) -> Just (s, tName) + (TargetProc (QualifiedIdentifier s pName) ) -> Just (s, tName) where - retType = pdReturnType <$> M.lookup proc allProcs - tName = case retType of + tName = case pdReturnType <$> proc of Just (SetOf (Composite qi)) -> qiName qi Just (Single (Composite qi)) -> qiName qi - _ -> proc + _ -> pName _ -> Nothing @@ -327,11 +325,6 @@ mutateRequest apiRequest fldNames = mapLeft apiRequestError $ (mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest) 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 (Node (sel, _) forest) = map (fst . view _1) (select sel) ++ map colName fks diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 25b024b75..cec3c7623 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -103,9 +103,10 @@ decodeSynonyms cols = <*> 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 = - 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 tblRow = ProcDescription <$> HD.value HD.text @@ -152,10 +153,10 @@ decodeProcs = | v == 's' = Stable | 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 -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 where sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')" diff --git a/src/PostgREST/Parsers.hs b/src/PostgREST/Parsers.hs index 1b4edf25c..769b0eaa7 100644 --- a/src/PostgREST/Parsers.hs +++ b/src/PostgREST/Parsers.hs @@ -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=(.. , ..)" 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 = toS <$> many (oneOf " \t") diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 713545d71..d4e625d3d 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -135,9 +135,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys type ProcResults = (Maybe Int64, Int64, ByteString, ByteString) 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) -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 where sql = @@ -164,15 +164,15 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para {responseHeaders} AS response_headers 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, "") | otherwise = ( - "_args_record AS ( "<> - "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) - ) + unwords [ + "_args_record AS (", + "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)) countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text _procName = qiName qi 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 -- 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. +-- The error also happens on raw libpq used with C. ignoredBody :: SqlFragment ignoredBody = "ignored_body AS (SELECT $1::text) " diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 8441a129e..474ee29cb 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -30,7 +30,8 @@ data DbStructure = DbStructure { , dbColumns :: [Column] , dbRelations :: [Relation] , dbPrimaryKeys :: [PrimaryKey] -, dbProcs :: M.HashMap Text ProcDescription +-- ProcDescription is a list because a function can be overloaded +, dbProcs :: M.HashMap Text [ProcDescription] , pgVersion :: PgVersion } deriving (Show, Eq) @@ -38,14 +39,14 @@ data PgArg = PgArg { pgaName :: Text , pgaType :: Text , 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 - deriving (Eq, Show) + deriving (Eq, Show, Ord) data ProcDescription = ProcDescription { pdName :: Text @@ -55,6 +56,13 @@ data ProcDescription = ProcDescription { , pdVolatility :: ProcVolatility } 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 TableName = Text type SqlQuery = Text @@ -111,7 +119,7 @@ data OrderTerm = OrderTerm { data QualifiedIdentifier = QualifiedIdentifier { qiSchema :: Schema , qiName :: TableName -} deriving (Show, Eq) +} deriving (Show, Eq, Ord) data RelationType = Child | Parent | Many | Root deriving (Show, Eq) diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 68853d871..3fef1119f 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -416,13 +416,21 @@ spec = do -- put value back for other tests 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| {} |] `shouldRespondWith` "" { matchStatus = 204, matchHeaders = ["Content-Range" <:> "*/*"] } + + request methodPatch "/items" [] [json| [] |] + `shouldRespondWith` "" + { + matchStatus = 204, + matchHeaders = ["Content-Range" <:> "*/*"] + } + 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}]|] { matchHeaders = [matchContentTypeJson] } diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 02e60e07e..48da43e09 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -275,8 +275,14 @@ spec = [json|[{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}]|] { matchHeaders = [matchContentTypeJson] } it "returns a row result when there are many INOUT params" $ - get "/rpc/many_inout_params?num=1&str=two" `shouldRespondWith` - [json| [{"num":1,"str":"two","b":true}]|] { matchHeaders = [matchContentTypeJson] } + 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` + [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" $ get "/rpc/raise_pt402" @@ -291,7 +297,7 @@ spec = context "expects a single json object" $ do it "does not expand posted json into parameters" $ 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"} } |] { matchHeaders = [matchContentTypeJson] } @@ -304,6 +310,25 @@ spec = , "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |] { 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" $ it "gives a parse filter error if GET style proc args are specified" $ post "/rpc/sayhello?name=John" [json|{}|] `shouldRespondWith` 400 diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index a8206d027..df2b821c7 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1325,6 +1325,23 @@ $$ language sql; 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"}]'; $$ 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;