feat: sql handlers for custom media types
* test text/html and drop HtmlRawOutputSpec.hs * all tests passing, removed all pendingWith * make functions compatible with pg <= 12 * move custom media types tests to own spec * anyelement aggregate * apply aggregates without a final function * overriding works * overriding anyelement with particular agg * cannot override vendored media types * plan spec works with custom aggregate * renamed media types to make clear which ones are overridable * correct content negotiation with same weight * text/tab-separated-values media type * text/csv with BOM plus content-disposition header
This commit is contained in:
committed by
Steve Chavez
parent
4a90e9fbd9
commit
82b38341bf
+8
-8
@@ -14,11 +14,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
|
||||
- #2983, Add more data to `Server-Timing` header - @develop7
|
||||
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem
|
||||
- #2825, SQL handlers for custom media types - @steve-chavez
|
||||
- Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3015, Fix unnecessary count() on RPC returning single - @steve-chavez
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed [raw-media-types config](https://postgrest.org/en/v11.1/references/configuration.html#raw-media-types) - @steve-chavez
|
||||
- Removed `application/octet-stream`, `text/plain`, `text/xml` [builtin support for scalar results](https://postgrest.org/en/v11.1/references/api/resource_representation.html#scalar-function-response-format) - @steve-chavez
|
||||
- Removed default `application/openapi+json` media type for [db-root-spec](https://postgrest.org/en/v11.1/references/configuration.html#db-root-spec) - @steve-chavez
|
||||
|
||||
## [11.2.2] - 2023-10-25
|
||||
|
||||
### Fixed
|
||||
@@ -86,14 +94,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
+ The `/table?select=*,other!fk(*)` must be used to disambiguate
|
||||
+ The server aids in choosing the `!fk` by sending a `hint` on the error whenever an ambiguous request happens.
|
||||
|
||||
### Changed
|
||||
|
||||
- #1462, #1548, Removed [raw-media-types config](https://postgrest.org/en/v11.1/references/configuration.html#raw-media-types) - @steve-chavez
|
||||
+ Can be replaced with custom media types
|
||||
- #1462, #1548, Removed `application/octet-stream`, `text/plain`, `text/xml` [builtin support for scalar results](https://postgrest.org/en/v11.1/references/api/resource_representation.html#scalar-function-response-format) - @steve-chavez
|
||||
+ Can be replaced with custom media types
|
||||
- #1462, #1548, Removed default `application/openapi+json` media type for [db-root-spec](https://postgrest.org/en/v11.1/references/configuration.html#db-root-spec) - @steve-chavez
|
||||
|
||||
## [11.1.0] - 2023-06-07
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -204,11 +204,11 @@ test-suite spec
|
||||
Feature.OptionsSpec
|
||||
Feature.Query.AndOrParamsSpec
|
||||
Feature.Query.ComputedRelsSpec
|
||||
Feature.Query.CustomMediaSpec
|
||||
Feature.Query.DeleteSpec
|
||||
Feature.Query.EmbedDisambiguationSpec
|
||||
Feature.Query.EmbedInnerJoinSpec
|
||||
Feature.Query.ErrorSpec
|
||||
Feature.Query.HtmlRawOutputSpec
|
||||
Feature.Query.InsertSpec
|
||||
Feature.Query.JsonOperatorSpec
|
||||
Feature.Query.MultipleSchemaSpec
|
||||
|
||||
@@ -220,7 +220,7 @@ handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@A
|
||||
return $ pgrstResponse metrics pgrst
|
||||
|
||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
||||
(planTime', iPlan) <- withTiming $ liftEither $ Plan.inspectPlan conf apiReq
|
||||
(planTime', iPlan) <- withTiming $ liftEither $ Plan.inspectPlan apiReq
|
||||
(rsTime', oaiResult) <- withTiming $ runQuery roleIsoLvl (Plan.ipTxmode iPlan) $ Query.openApiQuery sCache pgVer conf tSchema
|
||||
(renderTime', pgrst) <- withTiming $ liftEither $ Response.openApiResponse (T.decodeUtf8 prettyVersion, docsVersion) headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
||||
let metrics = Map.fromList [(SMPlan, planTime'), (SMQuery, rsTime'), (SMRender, renderTime'), jwtTime]
|
||||
|
||||
@@ -91,7 +91,7 @@ instance PgrstError ApiRequestError where
|
||||
status SingularityError{} = HTTP.status406
|
||||
status PGRSTParseError = HTTP.status500
|
||||
|
||||
headers SingularityError{} = [MediaType.toContentType $ MTSingularJSON False]
|
||||
headers SingularityError{} = [MediaType.toContentType $ MTVndSingularJSON False]
|
||||
headers _ = mempty
|
||||
|
||||
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||
|
||||
+32
-42
@@ -1,9 +1,10 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
|
||||
module PostgREST.MediaType
|
||||
( MediaType(..)
|
||||
, MTPlanOption (..)
|
||||
, MTPlanFormat (..)
|
||||
, MTVndPlanOption (..)
|
||||
, MTVndPlanFormat (..)
|
||||
, toContentType
|
||||
, toMime
|
||||
, decodeMediaType
|
||||
@@ -19,8 +20,6 @@ import Protolude
|
||||
-- | Enumeration of currently supported media types
|
||||
data MediaType
|
||||
= MTApplicationJSON
|
||||
| MTArrayJSONStrip
|
||||
| MTSingularJSON Bool
|
||||
| MTGeoJSON
|
||||
| MTTextCSV
|
||||
| MTTextPlain
|
||||
@@ -30,32 +29,23 @@ data MediaType
|
||||
| MTOctetStream
|
||||
| MTAny
|
||||
| MTOther ByteString
|
||||
-- TODO MTPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
|
||||
| MTPlan MediaType MTPlanFormat [MTPlanOption]
|
||||
deriving Show
|
||||
instance Eq MediaType where
|
||||
MTApplicationJSON == MTApplicationJSON = True
|
||||
MTArrayJSONStrip == MTArrayJSONStrip = True
|
||||
MTSingularJSON x == MTSingularJSON y = x == y
|
||||
MTGeoJSON == MTGeoJSON = True
|
||||
MTTextCSV == MTTextCSV = True
|
||||
MTTextPlain == MTTextPlain = True
|
||||
MTTextXML == MTTextXML = True
|
||||
MTOpenAPI == MTOpenAPI = True
|
||||
MTUrlEncoded == MTUrlEncoded = True
|
||||
MTOctetStream == MTOctetStream = True
|
||||
MTAny == MTAny = True
|
||||
MTOther x == MTOther y = x == y
|
||||
MTPlan{} == MTPlan{} = True
|
||||
_ == _ = False
|
||||
-- vendored media types
|
||||
| MTVndArrayJSONStrip
|
||||
| MTVndSingularJSON Bool
|
||||
-- TODO MTVndPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
|
||||
| MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]
|
||||
deriving (Eq, Show, Generic)
|
||||
instance Hashable MediaType
|
||||
|
||||
data MTPlanOption
|
||||
data MTVndPlanOption
|
||||
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
|
||||
deriving (Eq, Show)
|
||||
deriving (Eq, Show, Generic)
|
||||
instance Hashable MTVndPlanOption
|
||||
|
||||
data MTPlanFormat
|
||||
data MTVndPlanFormat
|
||||
= PlanJSON | PlanText
|
||||
deriving (Eq, Show)
|
||||
deriving (Eq, Show, Generic)
|
||||
instance Hashable MTVndPlanFormat
|
||||
|
||||
-- | Convert MediaType to a Content-Type HTTP Header
|
||||
toContentType :: MediaType -> Header
|
||||
@@ -69,31 +59,31 @@ toContentType ct = (hContentType, toMime ct <> charset)
|
||||
-- | Convert from MediaType to a ByteString representing the mime type
|
||||
toMime :: MediaType -> ByteString
|
||||
toMime MTApplicationJSON = "application/json"
|
||||
toMime MTArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
|
||||
toMime MTVndArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
|
||||
toMime MTGeoJSON = "application/geo+json"
|
||||
toMime MTTextCSV = "text/csv"
|
||||
toMime MTTextPlain = "text/plain"
|
||||
toMime MTTextXML = "text/xml"
|
||||
toMime MTOpenAPI = "application/openapi+json"
|
||||
toMime (MTSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
|
||||
toMime (MTSingularJSON False) = "application/vnd.pgrst.object+json"
|
||||
toMime (MTVndSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
|
||||
toMime (MTVndSingularJSON False) = "application/vnd.pgrst.object+json"
|
||||
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
|
||||
toMime MTOctetStream = "application/octet-stream"
|
||||
toMime MTAny = "*/*"
|
||||
toMime (MTOther ct) = ct
|
||||
toMime (MTPlan mt fmt opts) =
|
||||
toMime (MTVndPlan mt fmt opts) =
|
||||
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
|
||||
("; for=\"" <> toMime mt <> "\"") <>
|
||||
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
|
||||
|
||||
toMimePlanOption :: MTPlanOption -> ByteString
|
||||
toMimePlanOption :: MTVndPlanOption -> ByteString
|
||||
toMimePlanOption PlanAnalyze = "analyze"
|
||||
toMimePlanOption PlanVerbose = "verbose"
|
||||
toMimePlanOption PlanSettings = "settings"
|
||||
toMimePlanOption PlanBuffers = "buffers"
|
||||
toMimePlanOption PlanWAL = "wal"
|
||||
|
||||
toMimePlanFormat :: MTPlanFormat -> ByteString
|
||||
toMimePlanFormat :: MTVndPlanFormat -> ByteString
|
||||
toMimePlanFormat PlanJSON = "json"
|
||||
toMimePlanFormat PlanText = "text"
|
||||
|
||||
@@ -103,25 +93,25 @@ toMimePlanFormat PlanText = "text"
|
||||
-- MTApplicationJSON
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.plan;"
|
||||
-- MTPlan MTApplicationJSON PlanText []
|
||||
-- MTVndPlan MTApplicationJSON PlanText []
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
|
||||
-- MTPlan MTApplicationJSON PlanText []
|
||||
-- MTVndPlan MTApplicationJSON PlanText []
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
|
||||
-- MTPlan MTTextCSV PlanJSON []
|
||||
-- MTVndPlan MTTextCSV PlanJSON []
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
|
||||
-- MTArrayJSONStrip
|
||||
-- MTVndArrayJSONStrip
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
|
||||
-- MTApplicationJSON
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
|
||||
-- MTSingularJSON True
|
||||
-- MTVndSingularJSON True
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
|
||||
-- MTSingularJSON False
|
||||
-- MTVndSingularJSON False
|
||||
|
||||
decodeMediaType :: BS.ByteString -> MediaType
|
||||
decodeMediaType mt =
|
||||
@@ -145,11 +135,11 @@ decodeMediaType mt =
|
||||
other:_ -> MTOther other
|
||||
_ -> MTAny
|
||||
where
|
||||
checkArrayNullStrip ["nulls=stripped"] = MTArrayJSONStrip
|
||||
checkArrayNullStrip ["nulls=stripped"] = MTVndArrayJSONStrip
|
||||
checkArrayNullStrip _ = MTApplicationJSON
|
||||
|
||||
checkSingularNullStrip ["nulls=stripped"] = MTSingularJSON True
|
||||
checkSingularNullStrip _ = MTSingularJSON False
|
||||
checkSingularNullStrip ["nulls=stripped"] = MTVndSingularJSON True
|
||||
checkSingularNullStrip _ = MTVndSingularJSON False
|
||||
|
||||
getPlan fmt rest =
|
||||
let
|
||||
@@ -161,7 +151,7 @@ decodeMediaType mt =
|
||||
strippedFor <- BS.stripPrefix "for=" foundFor
|
||||
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor
|
||||
in
|
||||
MTPlan mtFor fmt $
|
||||
MTVndPlan mtFor fmt $
|
||||
[PlanAnalyze | inOpts "analyze" ] ++
|
||||
[PlanVerbose | inOpts "verbose" ] ++
|
||||
[PlanSettings | inOpts "settings"] ++
|
||||
|
||||
+50
-61
@@ -44,8 +44,7 @@ import PostgREST.ApiRequest (Action (..),
|
||||
Payload (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
import PostgREST.MediaType (MTPlanFormat (..),
|
||||
MediaType (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
@@ -53,6 +52,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
RelIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
@@ -61,7 +61,7 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
relIsToOne)
|
||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||
RepresentationsMap)
|
||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
Routine (..),
|
||||
RoutineMap,
|
||||
RoutineParam (..),
|
||||
@@ -93,16 +93,18 @@ import Protolude hiding (from)
|
||||
data WrappedReadPlan = WrappedReadPlan {
|
||||
wrReadPlan :: ReadPlanTree
|
||||
, wrTxMode :: SQL.Mode
|
||||
, wrResAgg :: ResultAggregate
|
||||
, wrHandler :: MediaHandler
|
||||
, wrMedia :: MediaType
|
||||
, wrIdent :: QualifiedIdentifier
|
||||
}
|
||||
|
||||
data MutateReadPlan = MutateReadPlan {
|
||||
mrReadPlan :: ReadPlanTree
|
||||
, mrMutatePlan :: MutatePlan
|
||||
, mrTxMode :: SQL.Mode
|
||||
, mrResAgg :: ResultAggregate
|
||||
, mrHandler :: MediaHandler
|
||||
, mrMedia :: MediaType
|
||||
, mrIdent :: QualifiedIdentifier
|
||||
}
|
||||
|
||||
data CallReadPlan = CallReadPlan {
|
||||
@@ -110,8 +112,9 @@ data CallReadPlan = CallReadPlan {
|
||||
, crCallPlan :: CallPlan
|
||||
, crTxMode :: SQL.Mode
|
||||
, crProc :: Routine
|
||||
, crResAgg :: ResultAggregate
|
||||
, crHandler :: MediaHandler
|
||||
, crMedia :: MediaType
|
||||
, crIdent :: QualifiedIdentifier
|
||||
}
|
||||
|
||||
data InspectPlan = InspectPlan {
|
||||
@@ -122,17 +125,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
|
||||
mediaType <- mapLeft ApiRequestError $ negotiateContent conf iAction iAcceptMediaType
|
||||
(hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
return $ WrappedReadPlan rPlan SQL.Read (mediaToAggregate mediaType apiRequest) mediaType
|
||||
return $ WrappedReadPlan rPlan SQL.Read hdler mediaType identifier
|
||||
|
||||
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
|
||||
mediaType <- mapLeft ApiRequestError $ negotiateContent conf iAction iAcceptMediaType
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write (mediaToAggregate mediaType apiRequest) mediaType
|
||||
(hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache)
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write hdler mediaType identifier
|
||||
|
||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
|
||||
@@ -156,15 +159,19 @@ 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
|
||||
mediaType <- mapLeft ApiRequestError $ negotiateContent conf iAction iAcceptMediaType
|
||||
(hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
return $ CallReadPlan rPlan cPlan txMode proc (mediaToAggregate mediaType apiRequest) mediaType
|
||||
return $ CallReadPlan rPlan cPlan txMode proc hdler mediaType relIdentifier
|
||||
where
|
||||
qsParams' = QueryParams.qsParams iQueryParams
|
||||
|
||||
inspectPlan :: AppConfig -> ApiRequest -> Either Error InspectPlan
|
||||
inspectPlan conf apiRequest = do
|
||||
mediaType <- mapLeft ApiRequestError $ negotiateContent conf (iAction apiRequest) (iAcceptMediaType apiRequest)
|
||||
inspectPlan :: ApiRequest -> Either Error InspectPlan
|
||||
inspectPlan apiRequest = do
|
||||
let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny]
|
||||
accepts = iAcceptMediaType apiRequest
|
||||
mediaType <- if not . null $ L.intersect accepts producedMTs
|
||||
then Right MTOpenAPI
|
||||
else Left . ApiRequestError . MediaTypeError $ MediaType.toMime <$> accepts
|
||||
return $ InspectPlan mediaType SQL.Read
|
||||
|
||||
{-|
|
||||
@@ -824,52 +831,34 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
||||
|
||||
mediaToAggregate :: MediaType -> ApiRequest -> ResultAggregate
|
||||
mediaToAggregate mt apiReq@ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} =
|
||||
if noAgg then NoAgg
|
||||
else case mt of
|
||||
MTApplicationJSON -> BuiltinAggJson
|
||||
MTSingularJSON strip -> BuiltinAggSingleJson strip
|
||||
MTArrayJSONStrip -> BuiltinAggArrayJsonStrip
|
||||
MTGeoJSON -> BuiltinAggGeoJson
|
||||
MTTextCSV -> BuiltinAggCsv
|
||||
MTAny -> BuiltinAggJson
|
||||
MTOpenAPI -> BuiltinAggJson
|
||||
MTUrlEncoded -> NoAgg -- TODO: unreachable since a previous step (producedMediaTypes) whitelists the media types that can become aggregates.
|
||||
|
||||
-- Doing `Accept: application/vnd.pgrst.plan; for="application/vnd.pgrst.plan"` doesn't make sense, so we just empty the body.
|
||||
-- TODO: fail instead to be more strict
|
||||
MTPlan (MTPlan{}) _ _ -> NoAgg
|
||||
MTPlan media _ _ -> mediaToAggregate media apiReq
|
||||
_ -> NoAgg
|
||||
where
|
||||
noAgg = case act of
|
||||
ActionMutate _ -> rep == Just HeadersOnly || rep == Just None || isNothing rep
|
||||
ActionRead _isHead -> _isHead -- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
ActionInvoke invMethod -> invMethod == InvHead
|
||||
_ -> False
|
||||
|
||||
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
|
||||
negotiateContent :: AppConfig -> Action -> [MediaType] -> Either ApiRequestError MediaType
|
||||
negotiateContent conf action accepts =
|
||||
case firstAcceptedPick of
|
||||
Just MTAny -> Right MTApplicationJSON -- by default(for */*) we respond with json
|
||||
Just mt -> Right mt
|
||||
Nothing -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] ->
|
||||
HM.HashMap (RelIdentifier, MediaType) MediaHandler -> Either ApiRequestError (MediaHandler, MediaType)
|
||||
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces =
|
||||
mtAnyToJSON $ 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)
|
||||
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
||||
(ActionRead True, Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(ActionInvoke InvHead, Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(_, Just (x, mt)) -> Right (x, mt)
|
||||
where
|
||||
-- TODO initial */* is not overridable
|
||||
-- initial handlers in the schema cache have a */* to BuiltinAggJson but they don't preserve the media type (application/json)
|
||||
-- for now we just convert the resultant */* to application/json here
|
||||
mtAnyToJSON = mapRight (\(x, y) -> (x, if y == MTAny then MTApplicationJSON else y))
|
||||
-- if there are multiple accepted media types, pick the first
|
||||
firstAcceptedPick = listToMaybe $ L.intersect accepts $ producedMediaTypes conf action
|
||||
|
||||
producedMediaTypes :: AppConfig -> Action -> [MediaType]
|
||||
producedMediaTypes conf action =
|
||||
case action of
|
||||
ActionRead _ -> defaultMediaTypes
|
||||
ActionInvoke _ -> defaultMediaTypes
|
||||
ActionInfo -> defaultMediaTypes
|
||||
ActionMutate _ -> defaultMediaTypes
|
||||
ActionInspect _ -> inspectMediaTypes
|
||||
where
|
||||
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
|
||||
defaultMediaTypes =
|
||||
[MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
|
||||
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
|
||||
firstAcceptedPick = listToMaybe $ mapMaybe searchMT accepts
|
||||
lookupIdent mt = -- first search for an aggregate that applies to the particular relation, then for one that applies to anyelement
|
||||
HM.lookup (RelId identifier, mt) produces <|> HM.lookup (RelAnyElement, mt) produces
|
||||
searchMT mt = case mt of
|
||||
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||
-- all the other media types can be overridden
|
||||
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ (,) <$> lookupIdent mType <*> pure m
|
||||
x -> (,) <$> lookupIdent x <*> pure x
|
||||
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||
|
||||
@@ -71,6 +71,7 @@ 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
|
||||
@@ -80,7 +81,7 @@ readQuery WrappedReadPlan{..} conf@AppConfig{..} apiReq@ApiRequest{iPreferences=
|
||||
)
|
||||
(shouldCount preferCount)
|
||||
wrMedia
|
||||
wrResAgg
|
||||
wrHandler
|
||||
configDbPreparedStatements
|
||||
failNotSingular wrMedia resultSet
|
||||
optionalRollback conf apiReq
|
||||
@@ -155,13 +156,14 @@ invokeQuery rout CallReadPlan{..} apiReq@ApiRequest{iPreferences=Preferences{..}
|
||||
resultSet <-
|
||||
lift . SQL.statement mempty $
|
||||
Statements.prepareCall
|
||||
crIdent
|
||||
rout
|
||||
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
|
||||
(QueryBuilder.readPlanToQuery crReadPlan)
|
||||
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
||||
(shouldCount preferCount)
|
||||
crMedia
|
||||
crResAgg
|
||||
crHandler
|
||||
configDbPreparedStatements
|
||||
|
||||
optionalRollback conf apiReq
|
||||
@@ -186,17 +188,18 @@ openApiQuery sCache pgVer AppConfig{..} tSchema =
|
||||
pure Nothing
|
||||
|
||||
writeQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
||||
writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan, mrResAgg, mrMedia} ApiRequest{iPreferences=Preferences{..}} conf =
|
||||
writeQuery MutateReadPlan{..} ApiRequest{iPreferences=Preferences{..}} conf =
|
||||
let
|
||||
(isInsert, pkCols) = case mrMutatePlan of {Insert{insPkCols} -> (True, insPkCols); _ -> (False, mempty);}
|
||||
in
|
||||
lift . SQL.statement mempty $
|
||||
Statements.prepareWrite
|
||||
mrIdent
|
||||
(QueryBuilder.readPlanToQuery mrReadPlan)
|
||||
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
||||
isInsert
|
||||
mrMedia
|
||||
mrResAgg
|
||||
mrHandler
|
||||
preferRepresentation
|
||||
pkCols
|
||||
(configDbPreparedStatements conf)
|
||||
@@ -207,7 +210,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan, mrResAgg, mrMedia} ApiReques
|
||||
failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
||||
failNotSingular _ RSPlan{} = pure ()
|
||||
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
||||
when (elem mediaType [MTSingularJSON True,MTSingularJSON False] && queryTotal /= 1) $ do
|
||||
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Description : Helper functions for PostgREST.QueryBuilder.
|
||||
-}
|
||||
module PostgREST.Query.SqlFragment
|
||||
( noLocationF
|
||||
, aggF
|
||||
, handlerF
|
||||
, countF
|
||||
, fromQi
|
||||
, limitOffsetF
|
||||
@@ -65,8 +65,8 @@ import PostgREST.ApiRequest.Types (Alias, Cast,
|
||||
QuantOperator (..),
|
||||
SimpleOperator (..),
|
||||
TrileanVal (..))
|
||||
import PostgREST.MediaType (MTPlanFormat (..),
|
||||
MTPlanOption (..))
|
||||
import PostgREST.MediaType (MTVndPlanFormat (..),
|
||||
MTVndPlanOption (..))
|
||||
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
||||
import PostgREST.Plan.Types (CoercibleField (..),
|
||||
CoercibleFilter (..),
|
||||
@@ -77,7 +77,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
rangeLimit, rangeOffset)
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
Routine (..),
|
||||
funcReturnsScalar,
|
||||
funcReturnsSetOfScalar,
|
||||
@@ -212,6 +212,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
|
||||
| (funcReturnsScalar <$> rout) == Just True = fromQi funcQi <> "(_postgrest_t.pgrst_scalar)"
|
||||
| otherwise = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")"
|
||||
|
||||
locationF :: [Text] -> SQL.Snippet
|
||||
locationF pKeys = [qc|(
|
||||
WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
|
||||
@@ -455,13 +460,13 @@ intercalateSnippet :: ByteString -> [SQL.Snippet] -> SQL.Snippet
|
||||
intercalateSnippet _ [] = mempty
|
||||
intercalateSnippet frag snippets = foldr1 (\a b -> a <> SQL.sql frag <> b) snippets
|
||||
|
||||
explainF :: MTPlanFormat -> [MTPlanOption] -> SQL.Snippet -> SQL.Snippet
|
||||
explainF :: MTVndPlanFormat -> [MTVndPlanOption] -> SQL.Snippet -> SQL.Snippet
|
||||
explainF fmt opts snip =
|
||||
"EXPLAIN (" <>
|
||||
SQL.sql (BS.intercalate ", " (fmtPlanFmt fmt : (fmtPlanOpt <$> opts))) <>
|
||||
") " <> snip
|
||||
where
|
||||
fmtPlanOpt :: MTPlanOption -> BS.ByteString
|
||||
fmtPlanOpt :: MTVndPlanOption -> BS.ByteString
|
||||
fmtPlanOpt PlanAnalyze = "ANALYZE"
|
||||
fmtPlanOpt PlanVerbose = "VERBOSE"
|
||||
fmtPlanOpt PlanSettings = "SETTINGS"
|
||||
@@ -486,11 +491,12 @@ setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal k
|
||||
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
|
||||
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
|
||||
|
||||
aggF :: Maybe Routine -> ResultAggregate -> SQL.Snippet
|
||||
aggF rout = \case
|
||||
BuiltinAggJson -> asJsonF rout False
|
||||
handlerF :: Maybe Routine -> QualifiedIdentifier -> MediaHandler -> SQL.Snippet
|
||||
handlerF rout target = \case
|
||||
BuiltinAggArrayJsonStrip -> asJsonF rout True
|
||||
BuiltinAggSingleJson strip -> asJsonSingleF rout strip
|
||||
BuiltinAggGeoJson -> asGeoJsonF
|
||||
BuiltinAggCsv -> asCsvF
|
||||
BuiltinOvAggJson -> asJsonF rout False
|
||||
BuiltinOvAggGeoJson -> asGeoJsonF
|
||||
BuiltinOvAggCsv -> asCsvF
|
||||
CustomFunc funcQi -> customFuncF rout funcQi target
|
||||
NoAgg -> "''::text"
|
||||
|
||||
@@ -25,11 +25,12 @@ import qualified Hasql.Statement as SQL
|
||||
import Control.Lens ((^?))
|
||||
|
||||
import PostgREST.ApiRequest.Preferences
|
||||
import PostgREST.MediaType (MTPlanFormat (..),
|
||||
MediaType (..))
|
||||
import PostgREST.MediaType (MTVndPlanFormat (..),
|
||||
MediaType (..))
|
||||
import PostgREST.Query.SqlFragment
|
||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
||||
Routine, funcReturnsSingle)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier)
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine,
|
||||
funcReturnsSingle)
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -53,9 +54,9 @@ data ResultSet
|
||||
| RSPlan BS.ByteString -- ^ the plan of the query
|
||||
|
||||
|
||||
prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> ResultAggregate ->
|
||||
prepareWrite :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler ->
|
||||
Maybe PreferRepresentation -> [Text] -> Bool -> SQL.Statement () ResultSet
|
||||
prepareWrite selectQuery mutateQuery isInsert mt rAgg rep pKeys =
|
||||
prepareWrite qi selectQuery mutateQuery isInsert mt handler rep pKeys =
|
||||
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||
where
|
||||
snippet =
|
||||
@@ -64,7 +65,7 @@ prepareWrite selectQuery mutateQuery isInsert mt rAgg rep pKeys =
|
||||
"'' AS total_result_set, " <>
|
||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||
locF <> " AS header, " <>
|
||||
aggF Nothing rAgg <> " AS body, " <>
|
||||
handlerF Nothing qi handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status " <>
|
||||
"FROM (" <> selectF <> ") _postgrest_t"
|
||||
@@ -80,16 +81,16 @@ prepareWrite selectQuery mutateQuery isInsert mt rAgg rep pKeys =
|
||||
|
||||
selectF
|
||||
-- prevent using any of the column names in ?select= when no response is returned from the CTE
|
||||
| rAgg == NoAgg = "SELECT * FROM " <> sourceCTE
|
||||
| otherwise = selectQuery
|
||||
| handler == NoAgg = "SELECT * FROM " <> sourceCTE
|
||||
| otherwise = selectQuery
|
||||
|
||||
decodeIt :: HD.Result ResultSet
|
||||
decodeIt = case mt of
|
||||
MTPlan{} -> planRow
|
||||
MTVndPlan{} -> planRow
|
||||
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing) <$> HD.rowMaybe (standardRow False)
|
||||
|
||||
prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> ResultAggregate -> Bool -> SQL.Statement () ResultSet
|
||||
prepareRead selectQuery countQuery countTotal mt rAgg =
|
||||
prepareRead :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet
|
||||
prepareRead qi selectQuery countQuery countTotal mt handler =
|
||||
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||
where
|
||||
snippet =
|
||||
@@ -98,7 +99,7 @@ prepareRead selectQuery countQuery countTotal mt rAgg =
|
||||
"SELECT " <>
|
||||
countResultF <> " AS total_result_set, " <>
|
||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||
aggF Nothing rAgg <> " AS body, " <>
|
||||
handlerF Nothing qi handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status " <>
|
||||
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
||||
@@ -107,13 +108,13 @@ prepareRead selectQuery countQuery countTotal mt rAgg =
|
||||
|
||||
decodeIt :: HD.Result ResultSet
|
||||
decodeIt = case mt of
|
||||
MTPlan{} -> planRow
|
||||
_ -> HD.singleRow $ standardRow True
|
||||
MTVndPlan{} -> planRow
|
||||
_ -> HD.singleRow $ standardRow True
|
||||
|
||||
prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||
MediaType -> ResultAggregate -> Bool ->
|
||||
prepareCall :: QualifiedIdentifier -> Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||
MediaType -> MediaHandler -> Bool ->
|
||||
SQL.Statement () ResultSet
|
||||
prepareCall rout callProcQuery selectQuery countQuery countTotal mt rAgg =
|
||||
prepareCall qi rout callProcQuery selectQuery countQuery countTotal mt handler =
|
||||
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||
where
|
||||
snippet =
|
||||
@@ -124,7 +125,7 @@ prepareCall rout callProcQuery selectQuery countQuery countTotal mt rAgg =
|
||||
(if funcReturnsSingle rout
|
||||
then "1"
|
||||
else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <>
|
||||
aggF (Just rout) rAgg <> " AS body, " <>
|
||||
handlerF (Just rout) qi handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status " <>
|
||||
"FROM (" <> selectQuery <> ") _postgrest_t"
|
||||
@@ -133,7 +134,7 @@ prepareCall rout callProcQuery selectQuery countQuery countTotal mt rAgg =
|
||||
|
||||
decodeIt :: HD.Result ResultSet
|
||||
decodeIt = case mt of
|
||||
MTPlan{} -> planRow
|
||||
MTVndPlan{} -> planRow
|
||||
_ -> fromMaybe (RSStandard (Just 0) 0 mempty mempty Nothing Nothing) <$> HD.rowMaybe (standardRow True)
|
||||
|
||||
preparePlanRows :: SQL.Snippet -> Bool -> SQL.Statement () (Maybe Int64)
|
||||
@@ -160,8 +161,8 @@ standardRow noLocation =
|
||||
|
||||
mtSnippet :: MediaType -> SQL.Snippet -> SQL.Snippet
|
||||
mtSnippet mediaType snippet = case mediaType of
|
||||
MTPlan _ fmt opts -> explainF fmt opts snippet
|
||||
_ -> snippet
|
||||
MTVndPlan _ fmt opts -> explainF fmt opts snippet
|
||||
_ -> snippet
|
||||
|
||||
-- | We use rowList because when doing EXPLAIN (FORMAT TEXT), the result comes as many rows. FORMAT JSON comes as one.
|
||||
planRow :: HD.Result ResultSet
|
||||
|
||||
@@ -350,7 +350,7 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
||||
& summary .~ pSum
|
||||
& description .~ mfilter (/="") pDesc
|
||||
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False]
|
||||
& produces ?~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False]
|
||||
& at 200 ?~ "OK"
|
||||
getOp = procOp
|
||||
& parameters .~ makeProcGetParams (pdParams pd)
|
||||
@@ -406,8 +406,8 @@ postgrestSpec (prettyVersion, docsVersion) rels pds ti (s, h, p, b) sd allowSecu
|
||||
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||
& parameters .~ fromList (makeParamDefs ti)
|
||||
& paths .~ makePathItems pds ti
|
||||
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
|
||||
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
|
||||
& produces .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]
|
||||
& consumes .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]
|
||||
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
|
||||
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
|
||||
where
|
||||
|
||||
@@ -28,7 +28,9 @@ module PostgREST.SchemaCache
|
||||
|
||||
import Control.Monad.Extra (whenJust)
|
||||
|
||||
import Data.Aeson ((.=))
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Types as JSON
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
||||
import qualified Data.Set as S
|
||||
@@ -48,7 +50,8 @@ import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||
pgVersion120)
|
||||
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema)
|
||||
RelIdentifier (..),
|
||||
Schema, isAnyElement)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..),
|
||||
@@ -56,6 +59,8 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||
RepresentationsMap)
|
||||
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
||||
MediaHandler (..),
|
||||
MediaHandlerMap,
|
||||
PgType (..),
|
||||
RetType (..),
|
||||
Routine (..),
|
||||
@@ -64,6 +69,8 @@ import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
||||
import PostgREST.SchemaCache.Table (Column (..), ColumnMap,
|
||||
Table (..), TablesMap)
|
||||
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
@@ -72,8 +79,16 @@ data SchemaCache = SchemaCache
|
||||
, dbRelationships :: RelationshipsMap
|
||||
, dbRoutines :: RoutineMap
|
||||
, dbRepresentations :: RepresentationsMap
|
||||
, dbMediaHandlers :: MediaHandlerMap
|
||||
}
|
||||
deriving (Generic, JSON.ToJSON)
|
||||
instance JSON.ToJSON SchemaCache where
|
||||
toJSON (SchemaCache tabs rels routs reps _) = JSON.object [
|
||||
"dbTables" .= JSON.toJSON tabs
|
||||
, "dbRelationships" .= JSON.toJSON rels
|
||||
, "dbRoutines" .= JSON.toJSON routs
|
||||
, "dbRepresentations" .= JSON.toJSON reps
|
||||
, "dbMediaHandlers" .= JSON.emptyArray
|
||||
]
|
||||
|
||||
-- | A view foreign key or primary key dependency detected on its source table
|
||||
-- Each column of the key could be referenced multiple times in the view, e.g.
|
||||
@@ -113,6 +128,7 @@ data KeyDep
|
||||
-- | A SQL query that can be executed independently
|
||||
type SqlQuery = ByteString
|
||||
|
||||
|
||||
querySchemaCache :: AppConfig -> SQL.Transaction SchemaCache
|
||||
querySchemaCache AppConfig{..} = do
|
||||
SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
|
||||
@@ -123,6 +139,7 @@ querySchemaCache AppConfig{..} = do
|
||||
funcs <- SQL.statement schemas $ allFunctions pgVer prepared
|
||||
cRels <- SQL.statement mempty $ allComputedRels prepared
|
||||
reps <- SQL.statement schemas $ dataRepresentations prepared
|
||||
mHdlers <- SQL.statement schemas $ mediaHandlers pgVer prepared
|
||||
_ <-
|
||||
let sleepCall = SQL.Statement "select pg_sleep($1)" (param HE.int4) HD.noResult prepared in
|
||||
whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing
|
||||
@@ -135,6 +152,7 @@ querySchemaCache AppConfig{..} = do
|
||||
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
||||
, dbRoutines = funcs
|
||||
, dbRepresentations = reps
|
||||
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
|
||||
}
|
||||
where
|
||||
schemas = toList configDbSchemas
|
||||
@@ -169,6 +187,7 @@ removeInternal schemas dbStruct =
|
||||
HM.filterWithKey (\(QualifiedIdentifier sch _, _) _ -> sch `elem` schemas ) (dbRelationships dbStruct)
|
||||
, dbRoutines = dbRoutines dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
|
||||
, dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API
|
||||
, dbMediaHandlers = dbMediaHandlers dbStruct
|
||||
}
|
||||
where
|
||||
hasInternalJunction ComputedRelationship{} = False
|
||||
@@ -1084,6 +1103,81 @@ allViewsKeyDependencies =
|
||||
having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
|
||||
|]
|
||||
|
||||
initialMediaHandlers :: MediaHandlerMap
|
||||
initialMediaHandlers =
|
||||
HM.insert (RelAnyElement, MediaType.MTAny ) BuiltinOvAggJson $
|
||||
HM.insert (RelAnyElement, MediaType.MTApplicationJSON) BuiltinOvAggJson $
|
||||
HM.insert (RelAnyElement, MediaType.MTTextCSV ) BuiltinOvAggCsv $
|
||||
HM.insert (RelAnyElement, MediaType.MTGeoJSON ) BuiltinOvAggGeoJson
|
||||
HM.empty
|
||||
|
||||
mediaHandlers :: PgVersion -> Bool -> SQL.Statement [Schema] MediaHandlerMap
|
||||
mediaHandlers pgVer =
|
||||
SQL.Statement sql (arrayParam HE.text) decodeMediaHandlers
|
||||
where
|
||||
sql = [q|
|
||||
with
|
||||
all_relations as (
|
||||
select reltype
|
||||
from pg_class
|
||||
where relkind in ('v','r','m','f','p')
|
||||
union
|
||||
select oid
|
||||
from pg_type
|
||||
where typname = 'anyelement'
|
||||
),
|
||||
media_types as (
|
||||
SELECT
|
||||
t.oid,
|
||||
lower(t.typname) as typname,
|
||||
b.oid as base_oid,
|
||||
b.typname AS basetypname,
|
||||
t.typnamespace
|
||||
FROM pg_type t
|
||||
JOIN pg_type b ON t.typbasetype = b.oid
|
||||
WHERE
|
||||
t.typbasetype <> 0 and
|
||||
t.typname ~* '^[A-Za-z0-9.-]+/[A-Za-z0-9.\+-]+$'
|
||||
)
|
||||
select
|
||||
proc_schema.nspname as handler_schema,
|
||||
proc.proname as handler_name,
|
||||
arg_schema.nspname::text as target_schema,
|
||||
arg_name.typname::text as target_name,
|
||||
media_types.typname as media_type
|
||||
from media_types
|
||||
join pg_proc proc on proc.prorettype = media_types.oid
|
||||
join pg_namespace proc_schema on proc_schema.oid = proc.pronamespace
|
||||
join pg_aggregate agg on agg.aggfnoid = proc.oid
|
||||
join pg_type arg_name on arg_name.oid = proc.proargtypes[0]
|
||||
join pg_namespace arg_schema on arg_schema.oid = arg_name.typnamespace
|
||||
where
|
||||
proc_schema.nspname = ANY($1) and
|
||||
proc.pronargs = 1 and
|
||||
arg_name.oid in (select reltype from all_relations)
|
||||
union
|
||||
select
|
||||
typ_sch.nspname as handler_schema,
|
||||
mtype.typname as handler_name,
|
||||
pro_sch.nspname as target_schema,
|
||||
proname as target_name,
|
||||
mtype.typname as media_type
|
||||
from pg_proc proc
|
||||
join pg_namespace pro_sch on pro_sch.oid = proc.pronamespace
|
||||
join media_types mtype on proc.prorettype = mtype.oid
|
||||
join pg_namespace typ_sch on typ_sch.oid = mtype.typnamespace
|
||||
where NOT proretset
|
||||
|] <> (if pgVer >= pgVersion110 then " AND prokind = 'f'" else " AND NOT (proisagg OR proiswindow)")
|
||||
|
||||
decodeMediaHandlers :: HD.Result MediaHandlerMap
|
||||
decodeMediaHandlers =
|
||||
HM.fromList . fmap (\(x, y, z) -> ((if isAnyElement y then RelAnyElement else RelId y, z), CustomFunc x) ) <$> HD.rowList caggRow
|
||||
where
|
||||
caggRow = (,,)
|
||||
<$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
|
||||
<*> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
|
||||
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
|
||||
|
||||
param :: HE.Value a -> HE.Params a
|
||||
param = HE.param . HE.nonNullable
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
module PostgREST.SchemaCache.Identifiers
|
||||
( QualifiedIdentifier(..)
|
||||
, RelIdentifier(..)
|
||||
, isAnyElement
|
||||
, Schema
|
||||
, TableName
|
||||
, FieldName
|
||||
@@ -17,6 +19,9 @@ import qualified Data.Text as T
|
||||
|
||||
import Protolude
|
||||
|
||||
data RelIdentifier = RelId QualifiedIdentifier | RelAnyElement
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)
|
||||
instance Hashable RelIdentifier
|
||||
|
||||
-- | Represents a pg identifier with a prepended schema name "schema.table".
|
||||
-- When qiSchema is "", the schema is defined by the pg search_path.
|
||||
@@ -28,6 +33,9 @@ data QualifiedIdentifier = QualifiedIdentifier
|
||||
|
||||
instance Hashable QualifiedIdentifier
|
||||
|
||||
isAnyElement :: QualifiedIdentifier -> Bool
|
||||
isAnyElement y = QualifiedIdentifier "pg_catalog" "anyelement" == y
|
||||
|
||||
dumpQi :: QualifiedIdentifier -> Text
|
||||
dumpQi (QualifiedIdentifier s i) =
|
||||
(if T.null s then mempty else s <> ".") <> i
|
||||
|
||||
@@ -15,16 +15,20 @@ module PostgREST.SchemaCache.Routine
|
||||
, funcTableName
|
||||
, funcReturnsCompositeAlias
|
||||
, funcReturnsSingle
|
||||
, ResultAggregate(..)
|
||||
, MediaHandlerMap
|
||||
, MediaHandler(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=))
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
Schema, TableName)
|
||||
RelIdentifier (..), Schema,
|
||||
TableName)
|
||||
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -88,12 +92,17 @@ instance Ord Routine where
|
||||
-- | It uses a HashMap for a faster lookup.
|
||||
type RoutineMap = HM.HashMap QualifiedIdentifier [Routine]
|
||||
|
||||
data ResultAggregate
|
||||
= BuiltinAggJson
|
||||
| BuiltinAggSingleJson Bool
|
||||
-- | A media handler can be an aggregate over a composite type or a function over a scalar
|
||||
data MediaHandler
|
||||
-- non overridable builtins
|
||||
= BuiltinAggSingleJson Bool
|
||||
| BuiltinAggArrayJsonStrip
|
||||
| BuiltinAggGeoJson
|
||||
| BuiltinAggCsv
|
||||
-- these builtins are overridable
|
||||
| BuiltinOvAggJson
|
||||
| BuiltinOvAggGeoJson
|
||||
| BuiltinOvAggCsv
|
||||
-- custom
|
||||
| CustomFunc QualifiedIdentifier
|
||||
| NoAgg
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -133,3 +142,5 @@ funcTableName proc = case pdReturnType proc of
|
||||
SetOf (Composite qi _) -> Just $ qiName qi
|
||||
Single (Composite qi _) -> Just $ qiName qi
|
||||
_ -> Nothing
|
||||
|
||||
type MediaHandlerMap = HM.HashMap (RelIdentifier, MediaType.MediaType) MediaHandler
|
||||
|
||||
@@ -102,7 +102,7 @@ postJsonArrayTest(){
|
||||
|
||||
echo "Running memory usage tests.."
|
||||
|
||||
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "23M"
|
||||
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "24M"
|
||||
jsonKeyTest "1M" "POST" "/leak?columns=blob" "16M"
|
||||
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "16M"
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ spec :: SpecWith ((), Application)
|
||||
spec =
|
||||
describe "root spec function" $ do
|
||||
it "accepts application/openapi+json" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/"
|
||||
[("Accept","application/openapi+json")] "" `shouldRespondWith`
|
||||
[json|{
|
||||
@@ -21,3 +20,12 @@ spec =
|
||||
"info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}
|
||||
}|]
|
||||
{ matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] }
|
||||
|
||||
it "accepts application/json" $ do
|
||||
request methodGet "/"
|
||||
[("Accept","application/json")] "" `shouldRespondWith`
|
||||
[json|{
|
||||
"swagger": "2.0",
|
||||
"info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}
|
||||
}|]
|
||||
{ matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] }
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
module Feature.Query.CustomMediaSpec where
|
||||
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Text.Heredoc (str)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import SpecHelper
|
||||
|
||||
spec :: SpecWith ((), Application)
|
||||
spec = describe "custom media types" $ do
|
||||
context "for tables with aggregate" $ do
|
||||
it "can query if there's an aggregate defined for the table" $ do
|
||||
r <- request methodGet "/lines" (acceptHdrs "application/vnd.twkb") ""
|
||||
liftIO $ do
|
||||
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
|
||||
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
|
||||
|
||||
it "can query by id if there's an aggregate defined for the table" $ do
|
||||
r <- request methodGet "/lines?id=eq.1" (acceptHdrs "application/vnd.twkb") ""
|
||||
liftIO $ do
|
||||
simpleBody r `shouldBe` readFixtureFile "1.twkb"
|
||||
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
|
||||
|
||||
it "will fail if there's no aggregate defined for the table" $ 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
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
it "can get raw xml output with Accept: text/xml if there's an aggregate defined" $ do
|
||||
request methodGet "/xmltest" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
"<myxml>foo</myxml>bar<foobar><baz/></foobar>"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
||||
}
|
||||
|
||||
-- TODO SOH (start of heading) is being added to results
|
||||
context "for tables with anyelement aggregate" $ do
|
||||
it "will use the application/vnd.geo2+json media type for any table" $
|
||||
request methodGet "/lines" (acceptHdrs "application/vnd.geo2+json") ""
|
||||
`shouldRespondWith`
|
||||
"\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/vnd.geo2+json"]
|
||||
}
|
||||
|
||||
it "will use the more specific application/vnd.geo2 handler for this table" $ do
|
||||
request methodGet "/shop_bles" (acceptHdrs "application/vnd.geo2+json") ""
|
||||
`shouldRespondWith`
|
||||
"\SOH\"anyelement overridden\""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/vnd.geo2+json"]
|
||||
}
|
||||
|
||||
request methodGet "/rpc/get_shop_bles" (acceptHdrs "application/vnd.geo2+json") ""
|
||||
`shouldRespondWith`
|
||||
"\SOH\"anyelement overridden\""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/vnd.geo2+json"]
|
||||
}
|
||||
|
||||
context "Proc that returns scalar" $ do
|
||||
it "can get raw output with Accept: text/html" $ do
|
||||
request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""
|
||||
`shouldRespondWith`
|
||||
[str|
|
||||
|<html>
|
||||
| <head>
|
||||
| <title>PostgREST</title>
|
||||
| </head>
|
||||
| <body>
|
||||
| <h1>Welcome to PostgREST</h1>
|
||||
| </body>
|
||||
|</html>
|
||||
|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/html"]
|
||||
}
|
||||
|
||||
it "can get raw output with Accept: text/plain" $ do
|
||||
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
|
||||
`shouldRespondWith` "Welcome to PostgREST"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "can get raw xml output with Accept: text/xml" $ do
|
||||
request methodGet "/rpc/return_scalar_xml" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
"<my-xml-tag/>"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "can get raw xml output with Accept: text/xml" $ do
|
||||
request methodGet "/rpc/welcome.xml" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
"<html>\n <head>\n <title>PostgREST</title>\n </head>\n <body>\n <h1>Welcome to PostgREST</h1>\n </body>\n</html>"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "should fail with function returning text and Accept: text/xml" $ do
|
||||
request methodGet "/rpc/welcome" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
[json|
|
||||
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/xml"}
|
||||
|]
|
||||
{ matchStatus = 415
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||
}
|
||||
|
||||
context "Proc that returns scalar based on a table" $ do
|
||||
it "can get an image with Accept: image/png" $ do
|
||||
r <- request methodGet "/rpc/ret_image" (acceptHdrs "image/png") ""
|
||||
liftIO $ do
|
||||
simpleBody r `shouldBe` readFixtureFile "A.png"
|
||||
simpleHeaders r `shouldContain` [("Content-Type", "image/png")]
|
||||
|
||||
context "Proc that returns set of scalars and Accept: text/plain" $
|
||||
it "will err because only scalars work with media type domains" $ do
|
||||
request methodGet "/rpc/welcome_twice"
|
||||
(acceptHdrs "text/plain")
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|]
|
||||
{ matchStatus = 415
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||
}
|
||||
|
||||
context "Proc that returns rows and accepts custom media type" $ do
|
||||
it "works if it has an aggregate defined" $ do
|
||||
r <- request methodGet "/rpc/get_lines" [("Accept", "application/vnd.twkb")] ""
|
||||
liftIO $ do
|
||||
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
|
||||
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
|
||||
|
||||
it "fails if doesn't have an aggregate defined" $ do
|
||||
request methodGet "/rpc/get_lines"
|
||||
(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 }
|
||||
|
||||
-- TODO SOH (start of heading) is being added to results
|
||||
it "works if there's an anyelement aggregate defined" $ do
|
||||
request methodGet "/rpc/get_lines" (acceptHdrs "application/vnd.geo2+json") ""
|
||||
`shouldRespondWith`
|
||||
"\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/vnd.geo2+json"]
|
||||
}
|
||||
|
||||
context "overriding" $ do
|
||||
it "will override the application/json handler for a single table" $
|
||||
request methodGet "/ov_json" (acceptHdrs "application/json") ""
|
||||
`shouldRespondWith`
|
||||
[json| {"overridden": "true"} |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||
}
|
||||
|
||||
-- TODO SOH (start of heading) is being added to results
|
||||
it "will override the application/geo+json handler for a single table" $
|
||||
request methodGet "/lines?id=eq.1" (acceptHdrs "application/geo+json") ""
|
||||
`shouldRespondWith`
|
||||
"\SOH{\"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:4326\"}}, \"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"LineString\", \"coordinates\": [[1, 1], [5, 5]]}, \"properties\": {\"id\": 1, \"name\": \"line-1\"}}]}"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "will not override vendored media types like application/vnd.pgrst.object" $
|
||||
request methodGet "/projects?id=eq.1" (acceptHdrs "application/vnd.pgrst.object") ""
|
||||
`shouldRespondWith`
|
||||
[json|{"id":1,"name":"Windows 7","client_id":1}|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||
}
|
||||
|
||||
context "matches requested media type correctly" $ do
|
||||
-- https://github.com/PostgREST/postgrest/issues/1462
|
||||
it "will match image/png according to q values" $ do
|
||||
r1 <- request methodGet "/rpc/ret_image" (acceptHdrs "image/png, */*") ""
|
||||
liftIO $ do
|
||||
simpleBody r1 `shouldBe` readFixtureFile "A.png"
|
||||
simpleHeaders r1 `shouldContain` [("Content-Type", "image/png")]
|
||||
|
||||
r2 <- request methodGet "/rpc/ret_image" (acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/png,*/*;q=0.8") ""
|
||||
liftIO $ do
|
||||
simpleBody r2 `shouldBe` readFixtureFile "A.png"
|
||||
simpleHeaders r2 `shouldContain` [("Content-Type", "image/png")]
|
||||
|
||||
-- https://github.com/PostgREST/postgrest/issues/2170
|
||||
it "will match json in presence of text/plain" $ do
|
||||
r <- request methodGet "/projects?id=eq.1" (acceptHdrs "text/plain, application/json") ""
|
||||
liftIO $ do
|
||||
simpleStatus r `shouldBe` status200
|
||||
simpleHeaders r `shouldContain` [("Content-Type", "application/json; charset=utf-8")]
|
||||
|
||||
-- https://github.com/PostgREST/postgrest/issues/1102
|
||||
it "will match a custom text/tab-separated-values" $ do
|
||||
request methodGet "/projects?id=in.(1,2)" (acceptHdrs "text/tab-separated-values") ""
|
||||
`shouldRespondWith`
|
||||
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/tab-separated-values"]
|
||||
}
|
||||
|
||||
-- https://github.com/PostgREST/postgrest/issues/1371#issuecomment-519248984
|
||||
it "will match a custom text/csv with BOM" $ do
|
||||
r <- request methodGet "/lines" (acceptHdrs "text/csv") ""
|
||||
liftIO $ do
|
||||
simpleBody r `shouldBe` readFixtureFile "lines.csv"
|
||||
simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")]
|
||||
simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")]
|
||||
@@ -1,31 +0,0 @@
|
||||
module Feature.Query.HtmlRawOutputSpec where
|
||||
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Text.Heredoc
|
||||
|
||||
import Protolude hiding (get)
|
||||
import SpecHelper (acceptHdrs)
|
||||
|
||||
spec :: SpecWith ((), Application)
|
||||
spec = describe "When raw-media-types is set to \"text/html\"" $
|
||||
it "can get raw output with Accept: text/html" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""
|
||||
`shouldRespondWith`
|
||||
[str|
|
||||
|<html>
|
||||
| <head>
|
||||
| <title>PostgREST</title>
|
||||
| </head>
|
||||
| <body>
|
||||
| <h1>Welcome to PostgREST</h1>
|
||||
| </body>
|
||||
|</html>
|
||||
|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/html"]
|
||||
}
|
||||
@@ -275,18 +275,6 @@ spec actualPgVersion = do
|
||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||
totalCost `shouldBe` 68.56
|
||||
|
||||
it "outputs the plan for text/xml" $ do
|
||||
pendingWith "TBD"
|
||||
r <- request methodGet "/rpc/return_scalar_xml"
|
||||
(acceptHdrs "application/vnd.pgrst.plan+json; for=\"text/xml\"; options=verbose") ""
|
||||
|
||||
let aggCol = simpleBody r ^? nth 0 . key "Plan" . key "Output" . nth 2
|
||||
resHeaders = simpleHeaders r
|
||||
|
||||
liftIO $ do
|
||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"text/xml\"; options=verbose; charset=utf-8")
|
||||
aggCol `shouldBe` Just [aesonQQ| "COALESCE(xmlagg(return_scalar_xml.pgrst_scalar), ''::xml)" |]
|
||||
|
||||
describe "text format" $ do
|
||||
it "outputs the total cost for a function call" $ do
|
||||
r <- request methodGet "/projects?id=in.(1,2,3)"
|
||||
@@ -442,6 +430,34 @@ spec actualPgVersion = do
|
||||
liftIO $ do
|
||||
resBody `shouldSatisfy` (\t -> T.isInfixOf "Index" (decodeUtf8 $ LBS.toStrict t))
|
||||
|
||||
describe "custom media types" $ do
|
||||
it "outputs the plan for a scalar function text/xml" $ do
|
||||
r <- request methodGet "/rpc/return_scalar_xml"
|
||||
(acceptHdrs "application/vnd.pgrst.plan+json; for=\"text/xml\"; options=verbose") ""
|
||||
|
||||
let aggCol = simpleBody r ^? nth 0 . key "Plan" . key "Output" . nth 2
|
||||
resHeaders = simpleHeaders r
|
||||
|
||||
liftIO $ do
|
||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"text/xml\"; options=verbose; charset=utf-8")
|
||||
aggCol `shouldBe` Just [aesonQQ| "return_scalar_xml.pgrst_scalar" |]
|
||||
|
||||
it "outputs the plan for an aggregate application/vnd.twkb" $ do
|
||||
r <- request methodGet "/lines"
|
||||
(acceptHdrs "application/vnd.pgrst.plan+json; for=\"application/vnd.twkb\"; options=verbose") ""
|
||||
|
||||
let aggCol = simpleBody r ^? nth 0 . key "Plan" . key "Output" . nth 2
|
||||
resHeaders = simpleHeaders r
|
||||
|
||||
liftIO $ do
|
||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.twkb\"; options=verbose; charset=utf-8")
|
||||
aggCol `shouldBe`
|
||||
(
|
||||
if actualPgVersion >= pgVersion120
|
||||
then Just [aesonQQ| "twkb_agg(ROW(lines.id, lines.name, lines.geom)::lines)" |]
|
||||
else Just [aesonQQ| "twkb_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.geom)::lines)" |]
|
||||
)
|
||||
|
||||
disabledSpec :: SpecWith ((), Application)
|
||||
disabledSpec =
|
||||
it "doesn't work if db-plan-enabled=false(the default)" $ do
|
||||
|
||||
@@ -1042,61 +1042,6 @@ spec actualPgVersion = do
|
||||
[json|[{"a$num$":100}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "binary output" $ do
|
||||
it "can query if a single column is selected" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream"]
|
||||
}
|
||||
|
||||
it "can get raw output with Accept: text/plain" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/projects?select=name&id=eq.1" (acceptHdrs "text/plain") ""
|
||||
`shouldRespondWith` "Windows 7"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "can get raw xml output with Accept: text/xml" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/xmltest?select=xml" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
"<myxml>foo</myxml>bar<foobar><baz/></foobar>"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "fails if a single column is not selected" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith`
|
||||
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST113","details":null,"hint":null} |]
|
||||
{ matchStatus = 406 }
|
||||
|
||||
request methodGet "/images?select=*&name=eq.A.png"
|
||||
(acceptHdrs "application/octet-stream")
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST113","details":null,"hint":null} |]
|
||||
{ matchStatus = 406 }
|
||||
|
||||
request methodGet "/images?name=eq.A.png"
|
||||
(acceptHdrs "application/octet-stream")
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST113","details":null,"hint":null} |]
|
||||
{ matchStatus = 406 }
|
||||
|
||||
it "concatenates results if more than one row is returned" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream"]
|
||||
}
|
||||
|
||||
describe "values with quotes in IN and NOT IN" $ do
|
||||
it "succeeds when only quoted values are present" $ do
|
||||
get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith`
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
module Feature.Query.RpcSpec where
|
||||
|
||||
import qualified Data.ByteString.Lazy as BL (empty, readFile)
|
||||
import qualified Data.ByteString.Lazy as BL (empty)
|
||||
|
||||
import Network.Wai (Application)
|
||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||
|
||||
import Network.HTTP.Types
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
@@ -1077,87 +1076,6 @@ spec actualPgVersion =
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
context "binary output" $ do
|
||||
context "Proc that returns scalar" $ do
|
||||
it "can query without selecting column" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream"]
|
||||
}
|
||||
|
||||
it "can get raw output with Accept: text/plain" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
|
||||
`shouldRespondWith` "Welcome to PostgREST"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "can get raw xml output with Accept: text/xml" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/rpc/return_scalar_xml" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
"<my-xml-tag/>"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "can get raw xml output with Accept: text/xml" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/rpc/welcome.xml" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
"<html>\n <head>\n <title>PostgREST</title>\n </head>\n <body>\n <h1>Welcome to PostgREST</h1>\n </body>\n</html>"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "should fail with function returning text and Accept: text/xml" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/rpc/welcome" (acceptHdrs "text/xml") ""
|
||||
`shouldRespondWith`
|
||||
[json|
|
||||
{
|
||||
"hint":"No function matches the given name and argument types. You might need to add explicit type casts.",
|
||||
"details":null,
|
||||
"code":"42883",
|
||||
"message":"function xmlagg(text) does not exist"
|
||||
}
|
||||
|]
|
||||
{ matchStatus = 406
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||
}
|
||||
|
||||
context "Proc that returns set of scalars" $
|
||||
it "can query without selecting column" $ do
|
||||
pendingWith "TBD"
|
||||
request methodGet "/rpc/welcome_twice"
|
||||
(acceptHdrs "text/plain")
|
||||
""
|
||||
`shouldRespondWith`
|
||||
"Welcome to PostgRESTWelcome to PostgREST"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
||||
}
|
||||
|
||||
context "Proc that returns rows" $ do
|
||||
it "can query if a single column is selected" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/octet-stream"]
|
||||
}
|
||||
|
||||
it "fails if a single column is not selected" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/ret_rows_with_base64_bin"
|
||||
(acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith`
|
||||
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST113","details":null,"hint":null} |]
|
||||
{ matchStatus = 406 }
|
||||
|
||||
context "only for GET rpc" $ do
|
||||
it "should fail on mutating procs" $ do
|
||||
get "/rpc/callcounter" `shouldRespondWith` 405
|
||||
@@ -1327,7 +1245,6 @@ spec actualPgVersion =
|
||||
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||
|
||||
it "can insert text directly" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/unnamed_text_param"
|
||||
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
|
||||
[str|unnamed text arg|]
|
||||
@@ -1335,7 +1252,6 @@ spec actualPgVersion =
|
||||
[str|unnamed text arg|]
|
||||
|
||||
it "can insert xml directly" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/unnamed_xml_param"
|
||||
[("Content-Type", "text/xml"), ("Accept", "text/xml")]
|
||||
[str|<note><from>John</from><to>Jane</to><message>Remember me</message></note>|]
|
||||
@@ -1343,8 +1259,7 @@ spec actualPgVersion =
|
||||
[str|<note><from>John</from><to>Jane</to><message>Remember me</message></note>|]
|
||||
|
||||
it "can insert bytea directly" $ do
|
||||
pendingWith "TBD"
|
||||
let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png"
|
||||
let file = readFixtureFile "image.png"
|
||||
r <- request methodPost "/rpc/unnamed_bytea_param"
|
||||
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
||||
file
|
||||
@@ -1397,10 +1312,9 @@ spec actualPgVersion =
|
||||
}
|
||||
|
||||
it "will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified" $
|
||||
let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png" in
|
||||
request methodPost "/rpc/unnamed_int_param"
|
||||
[("Content-Type", "application/octet-stream")]
|
||||
file
|
||||
(readFixtureFile "image.png")
|
||||
`shouldRespondWith`
|
||||
[json|{
|
||||
"hint": null,
|
||||
@@ -1429,7 +1343,6 @@ spec actualPgVersion =
|
||||
}
|
||||
|
||||
it "should be able to fallback to the single unnamed parameter function when other overloaded functions are not found" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/overloaded_unnamed_param"
|
||||
[("Content-Type", "application/json")]
|
||||
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||
@@ -1440,7 +1353,7 @@ spec actualPgVersion =
|
||||
[str|unnamed text arg|]
|
||||
`shouldRespondWith`
|
||||
[str|unnamed text arg|]
|
||||
let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png"
|
||||
let file = readFixtureFile "image.png"
|
||||
r <- request methodPost "/rpc/overloaded_unnamed_param"
|
||||
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
||||
file
|
||||
@@ -1488,7 +1401,6 @@ spec actualPgVersion =
|
||||
}
|
||||
|
||||
it "should fail on /rpc/unnamed_xml_param when posting invalid xml" $ do
|
||||
pendingWith "TBD"
|
||||
request methodPost "/rpc/unnamed_xml_param"
|
||||
[("Content-Type", "text/xml"), ("Accept", "text/xml")]
|
||||
[str|<|]
|
||||
|
||||
+2
-2
@@ -37,11 +37,11 @@ import qualified Feature.OpenApi.SecurityOpenApiSpec
|
||||
import qualified Feature.OptionsSpec
|
||||
import qualified Feature.Query.AndOrParamsSpec
|
||||
import qualified Feature.Query.ComputedRelsSpec
|
||||
import qualified Feature.Query.CustomMediaSpec
|
||||
import qualified Feature.Query.DeleteSpec
|
||||
import qualified Feature.Query.EmbedDisambiguationSpec
|
||||
import qualified Feature.Query.EmbedInnerJoinSpec
|
||||
import qualified Feature.Query.ErrorSpec
|
||||
import qualified Feature.Query.HtmlRawOutputSpec
|
||||
import qualified Feature.Query.InsertSpec
|
||||
import qualified Feature.Query.JsonOperatorSpec
|
||||
import qualified Feature.Query.MultipleSchemaSpec
|
||||
@@ -129,6 +129,7 @@ main = do
|
||||
, ("Feature.Auth.AuthSpec" , Feature.Auth.AuthSpec.spec actualPgVersion)
|
||||
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
|
||||
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
||||
, ("Feature.CustomMediaSpec" , Feature.Query.CustomMediaSpec.spec)
|
||||
, ("Feature.Query.DeleteSpec" , Feature.Query.DeleteSpec.spec)
|
||||
, ("Feature.Query.EmbedDisambiguationSpec" , Feature.Query.EmbedDisambiguationSpec.spec)
|
||||
, ("Feature.Query.EmbedInnerJoinSpec" , Feature.Query.EmbedInnerJoinSpec.spec)
|
||||
@@ -149,7 +150,6 @@ main = do
|
||||
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
|
||||
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
||||
, ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec)
|
||||
, ("Feature.Query.HtmlRawOutputSpec" , Feature.Query.HtmlRawOutputSpec.spec)
|
||||
, ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec)
|
||||
]
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import Data.CaseInsensitive (CI (..), mk, original)
|
||||
import Data.List (lookup)
|
||||
import Data.List.NonEmpty (fromList)
|
||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import System.Process (readProcess)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
|
||||
@@ -340,3 +341,6 @@ getInsertDataForTiobePlsTable rows =
|
||||
JSON.encode $ fromList $ [TiobePlsRow {name' = nm, rank = rk} | (nm,rk) <- nameRankList]
|
||||
where
|
||||
nameRankList = [("Lang " <> show i, i) | i <- [20..(rows+20)] ] :: [(Text, Int)]
|
||||
|
||||
readFixtureFile :: FilePath -> BL.ByteString
|
||||
readFixtureFile file = unsafePerformIO $ BL.readFile $ "test/spec/fixtures/" <> file
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 138 B |
Vendored
+3
@@ -858,3 +858,6 @@ TRUNCATE TABLE table_a CASCADE;
|
||||
INSERT INTO table_a(id, name) VALUES (1, 'Not null 1'), (2, null), (3, 'Not null 2');
|
||||
TRUNCATE TABLE table_b CASCADE;
|
||||
INSERT INTO table_b(table_a_id, name) VALUES (1, 'Test 1'), (2, 'Test 2'), (null, 'Test 3');
|
||||
|
||||
TRUNCATE TABLE lines CASCADE;
|
||||
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::extensions.geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::extensions.geometry);
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
id,name,geom
|
||||
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440
|
||||
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840
|
||||
|
Vendored
BIN
Binary file not shown.
Vendored
+211
-26
@@ -62,6 +62,7 @@ CREATE TYPE enum_menagerie_type AS ENUM (
|
||||
'bar'
|
||||
);
|
||||
|
||||
create type bit as enum ('one', 'two');
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
@@ -119,6 +120,20 @@ SET default_tablespace = '';
|
||||
|
||||
SET default_with_oids = false;
|
||||
|
||||
create domain "text/plain" as text;
|
||||
create domain "text/html" as text;
|
||||
create domain "text/xml" as pg_catalog.xml;
|
||||
create domain "application/octet-stream" as bytea;
|
||||
create domain "image/png" as bytea;
|
||||
create domain "application/vnd.twkb" as bytea;
|
||||
create domain "application/openapi+json" as json;
|
||||
create domain "application/geo+json" as jsonb;
|
||||
create domain "application/vnd.geo2+json" as jsonb;
|
||||
create domain "application/json" as json;
|
||||
create domain "application/vnd.pgrst.object" as json;
|
||||
create domain "text/tab-separated-values" as text;
|
||||
create domain "text/csv" as text;
|
||||
|
||||
CREATE TABLE items (
|
||||
id bigserial primary key
|
||||
);
|
||||
@@ -1160,12 +1175,8 @@ create or replace function test.ret_null() returns int as $$
|
||||
select null::int;
|
||||
$$ language sql;
|
||||
|
||||
create function test.ret_base64_bin() returns text as $$
|
||||
select i.img from test.images_base64 i where i.name = 'A.png';
|
||||
$$ language sql;
|
||||
|
||||
create function test.ret_rows_with_base64_bin() returns setof test.images_base64 as $$
|
||||
select i.name, i.img from test.images_base64 i;
|
||||
create function test.ret_image() returns "image/png" as $$
|
||||
select i.img::"image/png" from test.images i where i.name = 'A.png';
|
||||
$$ language sql;
|
||||
|
||||
create function test.single_article(id integer) returns test.articles as $$
|
||||
@@ -1896,7 +1907,7 @@ returns integer as $$
|
||||
select a + b;
|
||||
$$ language sql;
|
||||
|
||||
create or replace function root() returns json as $_$
|
||||
create or replace function root() returns "application/openapi+json" as $_$
|
||||
declare
|
||||
openapi json = $$
|
||||
{
|
||||
@@ -1912,17 +1923,17 @@ begin
|
||||
end
|
||||
$_$ language plpgsql;
|
||||
|
||||
create or replace function welcome() returns text as $$
|
||||
select 'Welcome to PostgREST'::text;
|
||||
create or replace function welcome() returns "text/plain" as $$
|
||||
select 'Welcome to PostgREST'::"text/plain";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function welcome_twice() returns setof text as $$
|
||||
create or replace function welcome_twice() returns setof "text/plain" as $$
|
||||
select 'Welcome to PostgREST'
|
||||
union all
|
||||
select 'Welcome to PostgREST';
|
||||
$$ language sql;
|
||||
|
||||
create or replace function "welcome.html"() returns text as $_$
|
||||
create or replace function "welcome.html"() returns "text/html" as $_$
|
||||
select $$
|
||||
<html>
|
||||
<head>
|
||||
@@ -1932,7 +1943,7 @@ select $$
|
||||
<h1>Welcome to PostgREST</h1>
|
||||
</body>
|
||||
</html>
|
||||
$$::text;
|
||||
$$::"text/html";
|
||||
$_$ language sql;
|
||||
|
||||
create view getallprojects_view as
|
||||
@@ -2370,16 +2381,16 @@ create or replace function test.unnamed_json_param(json) returns json as $$
|
||||
select $1;
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.unnamed_text_param(text) returns text as $$
|
||||
select $1;
|
||||
create or replace function test.unnamed_text_param(text) returns "text/plain" as $$
|
||||
select $1::"text/plain";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.unnamed_xml_param(pg_catalog.xml) returns pg_catalog.xml as $$
|
||||
select $1;
|
||||
create or replace function test.unnamed_xml_param(pg_catalog.xml) returns "text/xml" as $$
|
||||
select $1::"text/xml";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.unnamed_bytea_param(bytea) returns bytea as $$
|
||||
select $1::bytea;
|
||||
create or replace function test.unnamed_bytea_param(bytea) returns "application/octet-stream" as $$
|
||||
select $1::"application/octet-stream";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.unnamed_int_param(int) returns int as $$
|
||||
@@ -2390,12 +2401,12 @@ create or replace function test.overloaded_unnamed_param(json) returns json as $
|
||||
select $1;
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.overloaded_unnamed_param(bytea) returns bytea as $$
|
||||
select $1;
|
||||
create or replace function test.overloaded_unnamed_param(bytea) returns "application/octet-stream" as $$
|
||||
select $1::"application/octet-stream";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.overloaded_unnamed_param(text) returns text as $$
|
||||
select $1;
|
||||
create or replace function test.overloaded_unnamed_param(text) returns "text/plain" as $$
|
||||
select $1::"text/plain";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.overloaded_unnamed_param() returns int as $$
|
||||
@@ -2631,12 +2642,12 @@ create table plate_plan_step (
|
||||
REFERENCES well(well_id)
|
||||
);
|
||||
|
||||
CREATE FUNCTION test.return_scalar_xml() RETURNS pg_catalog.xml
|
||||
CREATE FUNCTION test.return_scalar_xml() RETURNS "text/xml"
|
||||
LANGUAGE sql AS $$
|
||||
SELECT '<my-xml-tag/>'::pg_catalog.xml
|
||||
SELECT '<my-xml-tag/>'::"text/xml"
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION "welcome.xml"() RETURNS pg_catalog.xml
|
||||
CREATE OR REPLACE FUNCTION "welcome.xml"() RETURNS "text/xml"
|
||||
LANGUAGE sql AS $_$
|
||||
select $$
|
||||
<html>
|
||||
@@ -2646,7 +2657,7 @@ select $$
|
||||
<body>
|
||||
<h1>Welcome to PostgREST</h1>
|
||||
</body>
|
||||
</html>$$::pg_catalog.xml;
|
||||
</html>$$::"text/xml";
|
||||
$_$;
|
||||
|
||||
CREATE TABLE test.xmltest (
|
||||
@@ -2654,6 +2665,23 @@ CREATE TABLE test.xmltest (
|
||||
xml pg_catalog.xml NOT NULL
|
||||
);
|
||||
|
||||
create or replace function test.xml_handler_transition (state "text/xml", next test.xmltest)
|
||||
returns "text/xml" as $$
|
||||
select xmlconcat2(state, next.xml)::"text/xml";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.xml_handler_final (data "text/xml")
|
||||
returns "text/xml" as $$
|
||||
select data;
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.text_xml_agg(test.xmltest);
|
||||
create aggregate test.text_xml_agg (test.xmltest) (
|
||||
stype = "text/xml"
|
||||
, sfunc = test.xml_handler_transition
|
||||
, finalfunc = test.xml_handler_final
|
||||
);
|
||||
|
||||
CREATE TABLE oid_test(
|
||||
id int,
|
||||
oid_col oid,
|
||||
@@ -3471,3 +3499,160 @@ stable
|
||||
as $$ begin
|
||||
return query select items2.id from items2 where items2.id=search2.id;
|
||||
end$$;
|
||||
|
||||
create table test.lines (
|
||||
id int primary key
|
||||
, name text
|
||||
, geom extensions.geometry(LINESTRING, 4326)
|
||||
);
|
||||
|
||||
create or replace function test.get_lines ()
|
||||
returns setof test.lines as $$
|
||||
select * from lines;
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.get_shop_bles ()
|
||||
returns setof test.shop_bles as $$
|
||||
select * from shop_bles;
|
||||
$$ language sql;
|
||||
|
||||
-- it can work without a final function too if the stype is already the media type
|
||||
create or replace function test.twkb_handler_transition (state "application/vnd.twkb", next test.lines)
|
||||
returns "application/vnd.twkb" as $$
|
||||
select (state || extensions.st_astwkb(next.geom)) :: "application/vnd.twkb";
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.twkb_agg(test.lines);
|
||||
create aggregate test.twkb_agg (test.lines) (
|
||||
initcond = ''
|
||||
, stype = "application/vnd.twkb"
|
||||
, sfunc = test.twkb_handler_transition
|
||||
);
|
||||
|
||||
create or replace function test.geo2json_trans (state "application/vnd.geo2+json", next anyelement)
|
||||
returns "application/vnd.geo2+json" as $$
|
||||
select (state || extensions.ST_AsGeoJSON(next)::jsonb)::"application/vnd.geo2+json";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.geo2json_final (data "application/vnd.geo2+json")
|
||||
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) (
|
||||
initcond = '[]'
|
||||
, stype = "application/vnd.geo2+json"
|
||||
, sfunc = geo2json_trans
|
||||
, finalfunc = geo2json_final
|
||||
);
|
||||
|
||||
create or replace function test.geo2json_trans (state "application/vnd.geo2+json", next test.shop_bles)
|
||||
returns "application/vnd.geo2+json" as $$
|
||||
select '"anyelement overridden"'::"application/vnd.geo2+json";
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.geo2json_agg(test.shop_bles);
|
||||
create aggregate test.geo2json_agg(test.shop_bles) (
|
||||
initcond = '[]'
|
||||
, stype = "application/vnd.geo2+json"
|
||||
, sfunc = geo2json_trans
|
||||
);
|
||||
|
||||
create table ov_json ();
|
||||
|
||||
-- override application/json
|
||||
create or replace function test.ov_json_trans (state "application/json", next ov_json)
|
||||
returns "application/json" as $$
|
||||
select null;
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.ov_json_agg(ov_json);
|
||||
create aggregate test.ov_json_agg(ov_json) (
|
||||
initcond = '{"overridden": "true"}'
|
||||
, stype = "application/json"
|
||||
, sfunc = ov_json_trans
|
||||
);
|
||||
|
||||
-- override application/geo+json
|
||||
create or replace function test.lines_geojson_trans (state jsonb, next test.lines)
|
||||
returns "application/geo+json" as $$
|
||||
select (state || extensions.ST_AsGeoJSON(next)::jsonb)::"application/geo+json";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.lines_geojson_final (data jsonb)
|
||||
returns "application/geo+json" as $$
|
||||
select jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'crs', json_build_object(
|
||||
'type', 'name',
|
||||
'properties', json_build_object(
|
||||
'name', 'EPSG:4326'
|
||||
)
|
||||
),
|
||||
'features', data
|
||||
)::"application/geo+json";
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.lines_geojson_agg(test.lines);
|
||||
create aggregate test.lines_geojson_agg (test.lines) (
|
||||
initcond = '[]'
|
||||
, stype = "application/geo+json"
|
||||
, sfunc = lines_geojson_trans
|
||||
, finalfunc = lines_geojson_final
|
||||
);
|
||||
|
||||
-- override application/vnd.pgrst.object
|
||||
create or replace function test.pgrst_obj_json_trans (state "application/vnd.pgrst.object", next anyelement)
|
||||
returns "application/vnd.pgrst.object" as $$
|
||||
select null;
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.pgrst_obj_agg(anyelement);
|
||||
create aggregate test.pgrst_obj_agg(anyelement) (
|
||||
initcond = '{"overridden": "true"}'
|
||||
, stype = "application/vnd.pgrst.object"
|
||||
, sfunc = pgrst_obj_json_trans
|
||||
);
|
||||
|
||||
-- create a "text/tab-separated-values" media type
|
||||
create or replace function test.tsv_trans (state text, next test.projects)
|
||||
returns "text/tab-separated-values" as $$
|
||||
select (state || next.id::text || E'\t' || next.name || E'\t' || coalesce(next.client_id::text, '') || E'\n')::"text/tab-separated-values";
|
||||
$$ language sql;
|
||||
|
||||
|
||||
create or replace function test.tsv_final (data "text/tab-separated-values")
|
||||
returns "text/tab-separated-values" as $$
|
||||
select set_config('response.headers', '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]', true);
|
||||
select (E'id\tname\tclient_id\n' || data)::"text/tab-separated-values";
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.tsv_agg(test.projects);
|
||||
create aggregate test.tsv_agg (test.projects) (
|
||||
initcond = ''
|
||||
, stype = "text/tab-separated-values"
|
||||
, sfunc = tsv_trans
|
||||
, finalfunc = tsv_final
|
||||
);
|
||||
|
||||
-- override CSV with BOM plus attachment
|
||||
create or replace function test.bom_csv_trans (state text, next test.lines)
|
||||
returns "text/csv" as $$
|
||||
select (state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\n')::"text/csv";
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.bom_csv_final (data "text/csv")
|
||||
returns "text/csv" as $$
|
||||
select set_config('response.headers', '[{"Content-Disposition": "attachment; filename=\"lines.csv\""}]', true);
|
||||
-- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
|
||||
select (convert_from (decode (E'EFBBBF', 'hex'),'UTF8') || (E'id,name,geom\n' || data))::"text/csv";
|
||||
$$ language sql;
|
||||
|
||||
drop aggregate if exists test.bom_csv_agg(test.lines);
|
||||
create aggregate test.bom_csv_agg (test.lines) (
|
||||
initcond = ''
|
||||
, stype = "text/csv"
|
||||
, sfunc = bom_csv_trans
|
||||
, finalfunc = bom_csv_final
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user