diff --git a/CHANGELOG.md b/CHANGELOG.md index 314932f887..f06dbef628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## [12.0.3] - 2024-05-09 + +### Fixed + + - #3149, Misleading "Starting PostgREST.." logs on schema cache reloading - @steve-chavez + - #3205, Fix wrong subquery error returning a status of 400 Bad Request - @steve-chavez + - #3224, Return status code 406 for non-accepted media type instead of code 415 - @wolfgangwalther + - #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther + - #3361, Clarify PGRST204(column not found) error message - @steve-chavez + - #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem + - #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla + - #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem + ## [12.0.2] - 2023-12-20 ### Fixed diff --git a/docs/references/errors.rst b/docs/references/errors.rst index f600100651..1a95386889 100644 --- a/docs/references/errors.rst +++ b/docs/references/errors.rst @@ -91,6 +91,8 @@ PostgREST translates `PostgreSQL error codes IO () run appState = do + AppState.logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..." + conf@AppConfig{..} <- AppState.getConfig appState AppState.connectionWorker appState -- Loads the initial SchemaCache Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.connectionWorker appState) (AppState.reReadConfig False appState) diff --git a/src/PostgREST/AppState.hs b/src/PostgREST/AppState.hs index 7a8dd05267..71903d8399 100644 --- a/src/PostgREST/AppState.hs +++ b/src/PostgREST/AppState.hs @@ -353,7 +353,6 @@ internalConnectionWorker appState = work where work = do config@AppConfig{..} <- getConfig appState - logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..." logWithZTime appState "Attempting to connect to the database..." connected <- establishConnection appState config case connected of diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 2d131ef175..84b2816608 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -65,7 +65,7 @@ instance PgrstError ApiRequestError where status AggregatesNotAllowed{} = HTTP.status400 status AmbiguousRelBetween{} = HTTP.status300 status AmbiguousRpc{} = HTTP.status300 - status MediaTypeError{} = HTTP.status415 + status MediaTypeError{} = HTTP.status406 status InvalidBody{} = HTTP.status400 status InvalidFilters = HTTP.status405 status InvalidPreferences{} = HTTP.status400 @@ -92,7 +92,6 @@ instance PgrstError ApiRequestError where status SingularityError{} = HTTP.status406 status PGRSTParseError = HTTP.status500 - headers SingularityError{} = [MediaType.toContentType $ MTVndSingularJSON False] headers _ = mempty toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value @@ -242,7 +241,7 @@ instance JSON.ToJSON ApiRequestError where (Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved") toJSON (ColumnNotFound relName colName) = toJsonPgrstError - SchemaCacheErrorCode04 ("Column '" <> colName <> "' of relation '" <> relName <> "' does not exist") Nothing Nothing + SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing -- | -- If no relationship is found then: @@ -457,6 +456,10 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError "23503" -> HTTP.status409 -- foreign_key_violation "23505" -> HTTP.status409 -- unique_violation "25006" -> HTTP.status405 -- read_only_sql_transaction + "21000" -> -- cardinality_violation + if BS.isSuffixOf "requires a WHERE clause" m + then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error + else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression" '2':'5':_ -> HTTP.status500 -- invalid tx state '2':'8':_ -> HTTP.status403 -- invalid auth specification '2':'D':_ -> HTTP.status500 -- invalid tx termination @@ -479,6 +482,7 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError then HTTP.status406 else HTTP.status404 -- undefined function "42P01" -> HTTP.status404 -- undefined table + "42P17" -> HTTP.status500 -- infinite recursion "42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege 'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m) "PGRST" -> diff --git a/src/PostgREST/Plan.hs b/src/PostgREST/Plan.hs index b948c88b48..564a6fe586 100644 --- a/src/PostgREST/Plan.hs +++ b/src/PostgREST/Plan.hs @@ -97,7 +97,6 @@ data WrappedReadPlan = WrappedReadPlan { , wrTxMode :: SQL.Mode , wrHandler :: MediaHandler , wrMedia :: MediaType -, wrIdent :: QualifiedIdentifier } data MutateReadPlan = MutateReadPlan { @@ -106,7 +105,6 @@ data MutateReadPlan = MutateReadPlan { , mrTxMode :: SQL.Mode , mrHandler :: MediaHandler , mrMedia :: MediaType -, mrIdent :: QualifiedIdentifier } data CallReadPlan = CallReadPlan { @@ -116,7 +114,6 @@ data CallReadPlan = CallReadPlan { , crProc :: Routine , crHandler :: MediaHandler , crMedia :: MediaType -, crIdent :: QualifiedIdentifier } data InspectPlan = InspectPlan { @@ -127,17 +124,17 @@ data InspectPlan = InspectPlan { wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do rPlan <- readPlan identifier conf sCache apiRequest - (hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) + (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan) if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () - return $ WrappedReadPlan rPlan SQL.Read hdler mediaType identifier + return $ WrappedReadPlan rPlan SQL.Read handler mediaType mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do rPlan <- readPlan identifier conf sCache apiRequest mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () - (hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) - return $ MutateReadPlan rPlan mPlan SQL.Write hdler mediaType identifier + (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan) + return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do @@ -161,12 +158,16 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc (InvPost, Routine.Immutable) -> SQL.Read (InvPost, Routine.Volatile) -> SQL.Write cPlan = callPlan proc apiRequest paramKeys args rPlan - (hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) + (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan) if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () - return $ CallReadPlan rPlan cPlan txMode proc hdler mediaType relIdentifier + return $ CallReadPlan rPlan cPlan txMode proc handler mediaType where qsParams' = QueryParams.qsParams iQueryParams +hasDefaultSelect :: ReadPlanTree -> Bool +hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*" +hasDefaultSelect _ = False + inspectPlan :: ApiRequest -> Either Error InspectPlan inspectPlan apiRequest = do let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny] @@ -993,8 +994,8 @@ addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleL addFilterToLogicForest flt lf = CoercibleStmnt flt : lf -- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types. -negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Either ApiRequestError ResolvedHandler -negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces = +negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler +negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect = case (act, firstAcceptedPick) of (_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts (ActionMutate _, Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt) @@ -1017,6 +1018,9 @@ negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRep x -> lookupHandler x mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed lookupHandler mt = - HM.lookup (RelId identifier, MTAny) produces <|> -- lookup for identifier and `*/*` - HM.lookup (RelId identifier, mt) produces <|> -- lookup for identifier and a particular media type - HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type + when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*` + when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type + HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type + when' :: Bool -> Maybe a -> Maybe a + when' True (Just a) = Just a + when' _ _ = Nothing diff --git a/src/PostgREST/Query.hs b/src/PostgREST/Query.hs index c8314f6c0d..077cfa6f8f 100644 --- a/src/PostgREST/Query.hs +++ b/src/PostgREST/Query.hs @@ -68,7 +68,6 @@ readQuery WrappedReadPlan{..} conf@AppConfig{..} apiReq@ApiRequest{iPreferences= resultSet <- lift . SQL.statement mempty $ Statements.prepareRead - wrIdent (QueryBuilder.readPlanToQuery wrReadPlan) (if preferCount == Just EstimatedCount then -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed @@ -153,7 +152,6 @@ invokeQuery rout CallReadPlan{..} apiReq@ApiRequest{iPreferences=Preferences{..} resultSet <- lift . SQL.statement mempty $ Statements.prepareCall - crIdent rout (QueryBuilder.callPlanToQuery crCallPlan pgVer) (QueryBuilder.readPlanToQuery crReadPlan) @@ -191,7 +189,6 @@ writeQuery MutateReadPlan{..} ApiRequest{iPreferences=Preferences{..}} conf = in lift . SQL.statement mempty $ Statements.prepareWrite - mrIdent (QueryBuilder.readPlanToQuery mrReadPlan) (QueryBuilder.mutatePlanToQuery mrMutatePlan) isInsert diff --git a/src/PostgREST/Query/SqlFragment.hs b/src/PostgREST/Query/SqlFragment.hs index 03f6177bd2..cdf28558aa 100644 --- a/src/PostgREST/Query/SqlFragment.hs +++ b/src/PostgREST/Query/SqlFragment.hs @@ -85,7 +85,8 @@ import PostgREST.Plan.Types (CoercibleField (..), import PostgREST.RangeQuery (NonnegRange, allRange, rangeLimit, rangeOffset) import PostgREST.SchemaCache.Identifiers (FieldName, - QualifiedIdentifier (..)) + QualifiedIdentifier (..), + RelIdentifier (..)) import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine (..), funcReturnsScalar, @@ -221,10 +222,11 @@ asJsonF rout strip asGeoJsonF :: SQL.Snippet asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))" -customFuncF :: Maybe Routine -> QualifiedIdentifier -> QualifiedIdentifier -> SQL.Snippet -customFuncF rout funcQi target +customFuncF :: Maybe Routine -> QualifiedIdentifier -> RelIdentifier -> SQL.Snippet +customFuncF rout funcQi _ | (funcReturnsScalar <$> rout) == Just True = fromQi funcQi <> "(_postgrest_t.pgrst_scalar)" - | otherwise = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")" +customFuncF _ funcQi RelAnyElement = fromQi funcQi <> "(_postgrest_t)" +customFuncF _ funcQi (RelId target) = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")" locationF :: [Text] -> SQL.Snippet locationF pKeys = [qc|( @@ -559,12 +561,12 @@ setConfigWithConstantNameJSON prefix keyVals = [setConfigWithConstantName (prefi arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)] arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal -handlerF :: Maybe Routine -> QualifiedIdentifier -> MediaHandler -> SQL.Snippet -handlerF rout target = \case +handlerF :: Maybe Routine -> MediaHandler -> SQL.Snippet +handlerF rout = \case BuiltinAggArrayJsonStrip -> asJsonF rout True BuiltinAggSingleJson strip -> asJsonSingleF rout strip BuiltinOvAggJson -> asJsonF rout False BuiltinOvAggGeoJson -> asGeoJsonF BuiltinOvAggCsv -> asCsvF - CustomFunc funcQi -> customFuncF rout funcQi target + CustomFunc funcQi target -> customFuncF rout funcQi target NoAgg -> "''::text" diff --git a/src/PostgREST/Query/Statements.hs b/src/PostgREST/Query/Statements.hs index 77947d04cd..e298347e37 100644 --- a/src/PostgREST/Query/Statements.hs +++ b/src/PostgREST/Query/Statements.hs @@ -25,12 +25,11 @@ import qualified Hasql.Statement as SQL import Control.Lens ((^?)) import PostgREST.ApiRequest.Preferences -import PostgREST.MediaType (MTVndPlanFormat (..), - MediaType (..)) +import PostgREST.MediaType (MTVndPlanFormat (..), + MediaType (..)) import PostgREST.Query.SqlFragment -import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier) -import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine, - funcReturnsSingle) +import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine, + funcReturnsSingle) import Protolude @@ -56,9 +55,9 @@ data ResultSet | RSPlan BS.ByteString -- ^ the plan of the query -prepareWrite :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler -> +prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler -> Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> SQL.Statement () ResultSet -prepareWrite qi selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys = +prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt where checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''" @@ -69,7 +68,7 @@ prepareWrite qi selectQuery mutateQuery isInsert isPut mt handler rep resolution "'' AS total_result_set, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <> locF <> " AS header, " <> - handlerF Nothing qi handler <> " AS body, " <> + handlerF Nothing handler <> " AS body, " <> responseHeadersF <> " AS response_headers, " <> responseStatusF <> " AS response_status, " <> pgrstInsertedF <> " AS response_inserted " <> @@ -94,8 +93,8 @@ prepareWrite qi selectQuery mutateQuery isInsert isPut mt handler rep resolution MTVndPlan{} -> planRow _ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False) -prepareRead :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet -prepareRead qi selectQuery countQuery countTotal mt handler = +prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet +prepareRead selectQuery countQuery countTotal mt handler = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt where snippet = @@ -104,7 +103,7 @@ prepareRead qi selectQuery countQuery countTotal mt handler = "SELECT " <> countResultF <> " AS total_result_set, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <> - handlerF Nothing qi handler <> " AS body, " <> + handlerF Nothing handler <> " AS body, " <> responseHeadersF <> " AS response_headers, " <> responseStatusF <> " AS response_status, " <> "''" <> " AS response_inserted " <> @@ -117,10 +116,10 @@ prepareRead qi selectQuery countQuery countTotal mt handler = MTVndPlan{} -> planRow _ -> HD.singleRow $ standardRow True -prepareCall :: QualifiedIdentifier -> Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool -> +prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet -prepareCall qi rout callProcQuery selectQuery countQuery countTotal mt handler = +prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt where snippet = @@ -131,7 +130,7 @@ prepareCall qi rout callProcQuery selectQuery countQuery countTotal mt handler = (if funcReturnsSingle rout then "1" else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <> - handlerF (Just rout) qi handler <> " AS body, " <> + handlerF (Just rout) handler <> " AS body, " <> responseHeadersF <> " AS response_headers, " <> responseStatusF <> " AS response_status, " <> "''" <> " AS response_inserted " <> diff --git a/src/PostgREST/Response/OpenAPI.hs b/src/PostgREST/Response/OpenAPI.hs index 0a89334544..8d939cd14a 100644 --- a/src/PostgREST/Response/OpenAPI.hs +++ b/src/PostgREST/Response/OpenAPI.hs @@ -115,6 +115,7 @@ makeProperty tbl rels col = (colName col, Inline s) -- Finds the relationship that has a single column foreign key rel = find (\case Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns) + Relationship{relCardinality=(O2O _ relColumns)} -> [colName col] == (fst <$> relColumns) _ -> False ) relsSortedByIsView fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel) diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index 65d0416d37..8b131e25ae 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -1188,7 +1188,9 @@ mediaHandlers pgVer = decodeMediaHandlers :: HD.Result MediaHandlerMap decodeMediaHandlers = - HM.fromList . fmap (\(x, y, z, w) -> ((if isAnyElement y then RelAnyElement else RelId y, z), (CustomFunc x, w)) ) <$> HD.rowList caggRow + HM.fromList . fmap (\(x, y, z, w) -> + let rel = if isAnyElement y then RelAnyElement else RelId y + in ((rel, z), (CustomFunc x rel, w)) ) <$> HD.rowList caggRow where caggRow = (,,,) <$> (QualifiedIdentifier <$> column HD.text <*> column HD.text) diff --git a/src/PostgREST/SchemaCache/Identifiers.hs b/src/PostgREST/SchemaCache/Identifiers.hs index 80993540ee..2cc01f2e9b 100644 --- a/src/PostgREST/SchemaCache/Identifiers.hs +++ b/src/PostgREST/SchemaCache/Identifiers.hs @@ -20,7 +20,7 @@ import qualified Data.Text as T import Protolude data RelIdentifier = RelId QualifiedIdentifier | RelAnyElement - deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey) + deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey, Show) instance Hashable RelIdentifier -- | Represents a pg identifier with a prepended schema name "schema.table". diff --git a/src/PostgREST/SchemaCache/Routine.hs b/src/PostgREST/SchemaCache/Routine.hs index e84d722b9a..90965661a4 100644 --- a/src/PostgREST/SchemaCache/Routine.hs +++ b/src/PostgREST/SchemaCache/Routine.hs @@ -105,7 +105,7 @@ data MediaHandler | BuiltinOvAggGeoJson | BuiltinOvAggCsv -- custom - | CustomFunc QualifiedIdentifier + | CustomFunc QualifiedIdentifier RelIdentifier | NoAgg deriving (Eq, Show) diff --git a/test/spec/Feature/OpenApi/OpenApiSpec.hs b/test/spec/Feature/OpenApi/OpenApiSpec.hs index e5b2e72b8a..0fc29508b5 100644 --- a/test/spec/Feature/OpenApi/OpenApiSpec.hs +++ b/test/spec/Feature/OpenApi/OpenApiSpec.hs @@ -30,15 +30,15 @@ spec actualPgVersion = describe "OpenAPI" $ do , matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] } - it "should respond to openapi request on none root path with 415" $ + it "should respond to openapi request on none root path with 406" $ request methodGet "/items" (acceptHdrs "application/openapi+json") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 - it "should respond to openapi request with unsupported media type with 415" $ + it "should respond to openapi request with unsupported media type with 406" $ request methodGet "/" (acceptHdrs "text/csv") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 it "includes postgrest.org current version api docs" $ do r <- simpleBody <$> get "/" @@ -222,6 +222,21 @@ spec actualPgVersion = describe "OpenAPI" $ do . nth 0 liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|] + it "includes a fk description for a O2O relationship" $ do + r <- simpleBody <$> get "/" + + let referralLink = r ^? key "definitions" . key "first" . key "properties" . key "second_id_1" + + liftIO $ + referralLink `shouldBe` Just + [aesonQQ| + { + "format": "integer", + "type": "integer", + "description": "Note:\nThis is a Foreign Key to `second.id`." + } + |] + describe "Foreign table" $ it "includes foreign table properties" $ do diff --git a/test/spec/Feature/Query/CustomMediaSpec.hs b/test/spec/Feature/Query/CustomMediaSpec.hs index c5c1786040..1b1773861e 100644 --- a/test/spec/Feature/Query/CustomMediaSpec.hs +++ b/test/spec/Feature/Query/CustomMediaSpec.hs @@ -31,7 +31,7 @@ spec = describe "custom media types" $ do request methodGet "/lines" (acceptHdrs "text/plain") "" `shouldRespondWith` [json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |] - { matchStatus = 415 + { matchStatus = 406 , matchHeaders = [matchContentTypeJson] } @@ -115,7 +115,7 @@ spec = describe "custom media types" $ do [json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/xml"} |] - { matchStatus = 415 + { matchStatus = 406 , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] } @@ -140,7 +140,7 @@ spec = describe "custom media types" $ do "" `shouldRespondWith` [json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|] - { matchStatus = 415 + { matchStatus = 406 , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] } @@ -156,7 +156,7 @@ spec = describe "custom media types" $ do (acceptHdrs "application/octet-stream") "" `shouldRespondWith` [json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: application/octet-stream"} |] - { matchStatus = 415 } + { matchStatus = 406 } -- TODO SOH (start of heading) is being added to results it "works if there's an anyelement aggregate defined" $ do @@ -230,6 +230,76 @@ spec = describe "custom media types" $ do simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")] simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")] + -- https://github.com/PostgREST/postgrest/issues/3160 + context "using select query parameter" $ do + it "without select" $ do + request methodGet "/projects?id=in.(1,2)" (acceptHdrs "pg/outfunc") "" + `shouldRespondWith` + [str|(1,"Windows 7",1) + |(2,"Windows 10",1) + |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "pg/outfunc"] + } + + it "with fewer columns selected" $ do + request methodGet "/projects?id=in.(1,2)&select=id,name" (acceptHdrs "pg/outfunc") "" + `shouldRespondWith` + [str|(1,"Windows 7") + |(2,"Windows 10") + |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "pg/outfunc"] + } + + it "with columns in different order" $ do + request methodGet "/projects?id=in.(1,2)&select=name,id,client_id" (acceptHdrs "pg/outfunc") "" + `shouldRespondWith` + [str|("Windows 7",1,1) + |("Windows 10",2,1) + |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "pg/outfunc"] + } + + it "with computed columns" $ do + request methodGet "/items?id=in.(1,2)&select=id,always_true" (acceptHdrs "pg/outfunc") "" + `shouldRespondWith` + [str|(1,t) + |(2,t) + |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "pg/outfunc"] + } + + -- TODO: Embeddings should not return JSON. Arrays of record would be much better. + it "with embedding" $ do + request methodGet "/projects?id=in.(1,2)&select=*,clients(id)" (acceptHdrs "pg/outfunc") "" + `shouldRespondWith` + [str|(1,"Windows 7",1,"{""id"": 1}") + |(2,"Windows 10",1,"{""id"": 1}") + |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "pg/outfunc"] + } + + it "will fail for specific aggregate with fewer columns" $ do + request methodGet "/lines?select=id" (acceptHdrs "application/vnd.twkb") "" + `shouldRespondWith` 406 + + it "will fail for specific aggregate with more columns" $ do + request methodGet "/lines?select=id,name,geom,id" (acceptHdrs "application/vnd.twkb") "" + `shouldRespondWith` 406 + + it "will fail for specific aggregate with columns in different order" $ do + request methodGet "/lines?select=name,id,geom" (acceptHdrs "application/vnd.twkb") "" + `shouldRespondWith` 406 + + -- This is just because it would be hard to detect this case, so we better error in this case, too. + it "will fail for specific aggregate with columns in same order" $ do + request methodGet "/lines?select=id,name,geom" (acceptHdrs "application/vnd.twkb") "" + `shouldRespondWith` 406 + context "any media type" $ do context "on functions" $ do it "returns application/json for */* if not explicitly set" $ do @@ -279,7 +349,7 @@ spec = describe "custom media types" $ do } request methodGet "/rpc/ret_some_mt" (acceptHdrs "text/csv") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 context "on tables" $ do it "returns application/json for */* if not explicitly set" $ do diff --git a/test/spec/Feature/Query/ErrorSpec.hs b/test/spec/Feature/Query/ErrorSpec.hs index 681694d232..feeb7f78f1 100644 --- a/test/spec/Feature/Query/ErrorSpec.hs +++ b/test/spec/Feature/Query/ErrorSpec.hs @@ -9,8 +9,8 @@ import Test.Hspec.Wai.JSON import Protolude hiding (get) -spec :: SpecWith ((), Application) -spec = do +nonExistentSchema :: SpecWith ((), Application) +nonExistentSchema = do describe "Non existent api schema" $ do it "succeeds when requesting root path" $ get "/" `shouldRespondWith` 200 @@ -61,3 +61,9 @@ spec = do "code": "PGRST117", "message":"Unsupported HTTP method: OTHER"}|] { matchStatus = 405 } + +pgErrorCodeMapping :: SpecWith ((), Application) +pgErrorCodeMapping = do + describe "PostreSQL error code mappings" $ do + it "should return 500 for cardinality_violation" $ + get "/bad_subquery" `shouldRespondWith` 500 diff --git a/test/spec/Feature/Query/InsertSpec.hs b/test/spec/Feature/Query/InsertSpec.hs index 817319912e..934c65b0f5 100644 --- a/test/spec/Feature/Query/InsertSpec.hs +++ b/test/spec/Feature/Query/InsertSpec.hs @@ -453,7 +453,7 @@ spec actualPgVersion = do {"id": 204, "body": "yyy"}, {"id": 205, "body": "zzz"}]|] `shouldRespondWith` - [json|{"code":"PGRST204","details":null,"hint":null,"message":"Column 'helicopter' of relation 'articles' does not exist"} |] + [json|{"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopter' column of 'articles' in the schema cache"} |] { matchStatus = 400 , matchHeaders = [] } @@ -835,7 +835,7 @@ spec actualPgVersion = do request methodPost "/datarep_todos?columns=id,label_color,helicopters&select=id,name,label_color,due_at" [("Prefer", "return=representation")] [json| {"due_at": "2019-01-03T11:00:00+00", "smth": "here", "label_color": "invalid", "fake_id": 13} |] `shouldRespondWith` - [json| {"code":"PGRST204","message":"Column 'helicopters' of relation 'datarep_todos' does not exist","details":null,"hint":null} |] + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos' in the schema cache"} |] { matchStatus = 400 , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] } @@ -890,7 +890,7 @@ spec actualPgVersion = do request methodPost "/datarep_todos_computed?columns=id,label_color,helicopters&select=id,name,label_color,due_at" [("Prefer", "return=representation")] [json| {"due_at": "2019-01-03T11:00:00+00", "smth": "here", "label_color": "invalid", "fake_id": 13} |] `shouldRespondWith` - [json| {"code":"PGRST204","message":"Column 'helicopters' of relation 'datarep_todos_computed' does not exist","details":null,"hint":null} |] + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache"} |] { matchStatus = 400 , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] } diff --git a/test/spec/Feature/Query/NullsStripSpec.hs b/test/spec/Feature/Query/NullsStripSpec.hs index 0a4b82c020..ef5c0c5787 100644 --- a/test/spec/Feature/Query/NullsStripSpec.hs +++ b/test/spec/Feature/Query/NullsStripSpec.hs @@ -72,7 +72,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 6 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } context "strip nulls from response even if explicitly selected" $ do diff --git a/test/spec/Feature/Query/PlanSpec.hs b/test/spec/Feature/Query/PlanSpec.hs index 704fe877e0..de63dccebf 100644 --- a/test/spec/Feature/Query/PlanSpec.hs +++ b/test/spec/Feature/Query/PlanSpec.hs @@ -463,12 +463,12 @@ disabledSpec = it "doesn't work if db-plan-enabled=false(the default)" $ do request methodGet "/projects?id=in.(1,2,3)" (acceptHdrs "application/vnd.pgrst.plan") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 request methodGet "/rpc/getallprojects?id=in.(1,2,3)" (acceptHdrs "application/vnd.pgrst.plan") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 request methodDelete "/projects?id=in.(1,2,3)" (acceptHdrs "application/vnd.pgrst.plan") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 diff --git a/test/spec/Feature/Query/QuerySpec.hs b/test/spec/Feature/Query/QuerySpec.hs index 073cb48249..3b1da167b6 100644 --- a/test/spec/Feature/Query/QuerySpec.hs +++ b/test/spec/Feature/Query/QuerySpec.hs @@ -939,12 +939,12 @@ spec actualPgVersion = do } describe "Accept headers" $ do - it "should respond an unknown accept type with 415" $ + it "should respond an unknown accept type with 406" $ request methodGet "/simple_pk" (acceptHdrs "text/unknowntype") "" `shouldRespondWith` [json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|] - { matchStatus = 415 + { matchStatus = 406 , matchHeaders = [matchContentTypeJson] } @@ -1407,3 +1407,10 @@ spec actualPgVersion = do { matchStatus = 200 , matchHeaders = [matchContentTypeJson] } + + context "test infinite recursion error 42P17" $ + it "return http status 500" $ + get "/infinite_recursion?select=*" `shouldRespondWith` + [json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|] + { matchStatus = 500 } + diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index fc14615791..4887e8face 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -656,10 +656,10 @@ spec actualPgVersion = it "rejects unknown content type even if payload is good" $ do request methodPost "/rpc/sayhello" (acceptHdrs "audio/mpeg3") [json| { "name": "world" } |] - `shouldRespondWith` 415 + `shouldRespondWith` 406 request methodGet "/rpc/sayhello?name=world" (acceptHdrs "audio/mpeg3") "" - `shouldRespondWith` 415 + `shouldRespondWith` 406 it "rejects malformed json payload" $ do p <- request methodPost "/rpc/sayhello" (acceptHdrs "application/json") "sdfsdf" diff --git a/test/spec/Feature/Query/SingularSpec.hs b/test/spec/Feature/Query/SingularSpec.hs index 5b5e1a3e7e..1f856166e4 100644 --- a/test/spec/Feature/Query/SingularSpec.hs +++ b/test/spec/Feature/Query/SingularSpec.hs @@ -72,7 +72,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 4 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should not be updated, either @@ -87,7 +87,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 4 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should not be updated, either @@ -101,7 +101,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } it "raises an error for zero rows with return=rep" $ @@ -110,7 +110,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } context "when creating rows" $ do @@ -143,7 +143,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should not exist, either @@ -158,7 +158,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should not exist, either @@ -173,7 +173,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should not exist, either @@ -188,7 +188,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } it "raises an error when creating zero entities with return=rep" $ @@ -198,7 +198,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } context "when deleting rows" $ do @@ -221,7 +221,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should still exist @@ -238,7 +238,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular ] + , matchHeaders = [ matchContentTypeJson ] } -- the rows should still exist @@ -254,7 +254,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } it "raises an error when deleting zero entities with return=rep" $ @@ -263,7 +263,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } context "when calling a stored proc" $ do @@ -273,7 +273,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 0 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } -- this one may be controversial, should vnd.pgrst.object include @@ -296,7 +296,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [matchContentTypeSingular] + , matchHeaders = [matchContentTypeJson] } it "fails for multiple rows with rolled back changes" $ do @@ -311,7 +311,7 @@ spec = `shouldRespondWith` [json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|] { matchStatus = 406 - , matchHeaders = [ matchContentTypeSingular] + , matchHeaders = [ matchContentTypeJson ] } -- should rollback function diff --git a/test/spec/Feature/Query/UpdateSpec.hs b/test/spec/Feature/Query/UpdateSpec.hs index 7cf7013041..dd2504d244 100644 --- a/test/spec/Feature/Query/UpdateSpec.hs +++ b/test/spec/Feature/Query/UpdateSpec.hs @@ -333,7 +333,7 @@ spec actualPgVersion = do [("Prefer", "return=representation")] [json|{"body": "yyy"}|] `shouldRespondWith` - [json|{"code":"PGRST204","details":null,"hint":null,"message":"Column 'helicopter' of relation 'articles' does not exist"} |] + [json|{"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopter' column of 'articles' in the schema cache"}|] { matchStatus = 400 , matchHeaders = [] } @@ -879,7 +879,7 @@ spec actualPgVersion = do request methodPatch "/datarep_todos?id=eq.2&columns=label_color,helicopters" [("Prefer", "return=representation")] [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] `shouldRespondWith` - [json| {"code":"PGRST204","message":"Column 'helicopters' of relation 'datarep_todos' does not exist","details":null,"hint":null} |] + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos' in the schema cache"} |] { matchStatus = 400 , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] } @@ -969,7 +969,7 @@ spec actualPgVersion = do request methodPatch "/datarep_todos_computed?id=eq.2&columns=label_color,helicopters" [("Prefer", "return=representation")] [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] `shouldRespondWith` - [json| {"code":"PGRST204","message":"Column 'helicopters' of relation 'datarep_todos_computed' does not exist","details":null,"hint":null} |] + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache"} |] { matchStatus = 400 , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] } diff --git a/test/spec/Main.hs b/test/spec/Main.hs index 4e5266e29f..8ae6cdb411 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -152,6 +152,7 @@ main = do , ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec) , ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec) , ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec) + , ("Feature.Query.PgErrorCodeMappingSpec" , Feature.Query.ErrorSpec.pgErrorCodeMapping) ] hspec $ do @@ -211,7 +212,7 @@ main = do -- this test runs with a nonexistent db-schema parallel $ before nonexistentSchemaApp $ - describe "Feature.Query.ErrorSpec" Feature.Query.ErrorSpec.spec + describe "Feature.Query.NonExistentSchemaErrorSpec" Feature.Query.ErrorSpec.nonExistentSchema -- this test runs with an extra search path parallel $ before extraSearchPathApp $ do diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index 0717ef12b4..362c61f4c0 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE FlexibleContexts #-} module SpecHelper where import Control.Lens ((^?)) @@ -36,17 +37,29 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import Protolude hiding (get, toS) import Protolude.Conv (toS) +filterAndMatchCT :: BS.ByteString -> MatchHeader +filterAndMatchCT val = MatchHeader $ \headers _ -> + case filter (\(n,_) -> n == hContentType) headers of + [(_,v)] -> if v == val + then Nothing + else Just $ "missing value:" <> toS val <> "\n" + _ -> Just "unexpected header: zero or multiple headers present\n" + matchContentTypeJson :: MatchHeader -matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8" +matchContentTypeJson = + filterAndMatchCT "application/json; charset=utf-8" matchContentTypeSingular :: MatchHeader -matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8" +matchContentTypeSingular = + filterAndMatchCT "application/vnd.pgrst.object+json; charset=utf-8" matchCTArrayStrip :: MatchHeader -matchCTArrayStrip = "Content-Type" <:> "application/vnd.pgrst.array+json;nulls=stripped; charset=utf-8" +matchCTArrayStrip = + filterAndMatchCT "application/vnd.pgrst.array+json;nulls=stripped; charset=utf-8" matchCTSingularStrip :: MatchHeader -matchCTSingularStrip = "Content-Type" <:> "application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8" +matchCTSingularStrip = + filterAndMatchCT "application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8" matchHeaderValuePresent :: HeaderName -> BS.ByteString -> MatchHeader matchHeaderValuePresent name val = MatchHeader $ \headers _ -> diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index 33f2eefc5b..7b7f84f2a3 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -3550,8 +3550,8 @@ returns "application/vnd.geo2+json" as $$ select (jsonb_build_object('type', 'FeatureCollection', 'hello', 'world'))::"application/vnd.geo2+json"; $$ language sql; -drop aggregate if exists test.geo2json_agg(anyelement); -create aggregate test.geo2json_agg(anyelement) ( +drop aggregate if exists test.geo2json_agg_any(anyelement); +create aggregate test.geo2json_agg_any(anyelement) ( initcond = '[]' , stype = "application/vnd.geo2+json" , sfunc = geo2json_trans @@ -3714,7 +3714,7 @@ begin perform set_config('response.headers', json_build_array(json_build_object('Content-Type', 'app/groucho'))::text, true); resp := 'groucho'; else - raise sqlstate 'PT415' using message = 'Unsupported Media Type'; + raise sqlstate 'PT406' using message = 'Not Acceptable'; end case; return resp; end; $$ language plpgsql; @@ -3752,3 +3752,26 @@ create aggregate test.some_agg (some_numbers) ( , sfunc = some_trans , finalfunc = some_final ); + +create view bad_subquery as +select * from projects where id = (select id from projects); + +-- custom generic mimetype +create domain "pg/outfunc" as text; +create function test.outfunc_trans (state text, next anyelement) +returns "pg/outfunc" as $$ + select (state || next::text || E'\n')::"pg/outfunc"; +$$ language sql; + +create aggregate test.outfunc_agg (anyelement) ( + initcond = '' +, stype = "pg/outfunc" +, sfunc = outfunc_trans +); + +-- https://github.com/PostgREST/postgrest/issues/3256 +create view test.infinite_recursion as +select * from test.projects; + +create or replace view test.infinite_recursion as +select * from test.infinite_recursion;