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
|
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
|
||||||
- #2983, Add more data to `Server-Timing` header - @develop7
|
- #2983, Add more data to `Server-Timing` header - @develop7
|
||||||
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem
|
- #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
|
### Fixed
|
||||||
|
|
||||||
- #3015, Fix unnecessary count() on RPC returning single - @steve-chavez
|
- #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
|
## [11.2.2] - 2023-10-25
|
||||||
|
|
||||||
### Fixed
|
### 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 `/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.
|
+ 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
|
## [11.1.0] - 2023-06-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -204,11 +204,11 @@ test-suite spec
|
|||||||
Feature.OptionsSpec
|
Feature.OptionsSpec
|
||||||
Feature.Query.AndOrParamsSpec
|
Feature.Query.AndOrParamsSpec
|
||||||
Feature.Query.ComputedRelsSpec
|
Feature.Query.ComputedRelsSpec
|
||||||
|
Feature.Query.CustomMediaSpec
|
||||||
Feature.Query.DeleteSpec
|
Feature.Query.DeleteSpec
|
||||||
Feature.Query.EmbedDisambiguationSpec
|
Feature.Query.EmbedDisambiguationSpec
|
||||||
Feature.Query.EmbedInnerJoinSpec
|
Feature.Query.EmbedInnerJoinSpec
|
||||||
Feature.Query.ErrorSpec
|
Feature.Query.ErrorSpec
|
||||||
Feature.Query.HtmlRawOutputSpec
|
|
||||||
Feature.Query.InsertSpec
|
Feature.Query.InsertSpec
|
||||||
Feature.Query.JsonOperatorSpec
|
Feature.Query.JsonOperatorSpec
|
||||||
Feature.Query.MultipleSchemaSpec
|
Feature.Query.MultipleSchemaSpec
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@A
|
|||||||
return $ pgrstResponse metrics pgrst
|
return $ pgrstResponse metrics pgrst
|
||||||
|
|
||||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
(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
|
(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
|
(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]
|
let metrics = Map.fromList [(SMPlan, planTime'), (SMQuery, rsTime'), (SMRender, renderTime'), jwtTime]
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ instance PgrstError ApiRequestError where
|
|||||||
status SingularityError{} = HTTP.status406
|
status SingularityError{} = HTTP.status406
|
||||||
status PGRSTParseError = HTTP.status500
|
status PGRSTParseError = HTTP.status500
|
||||||
|
|
||||||
headers SingularityError{} = [MediaType.toContentType $ MTSingularJSON False]
|
headers SingularityError{} = [MediaType.toContentType $ MTVndSingularJSON False]
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||||
|
|||||||
+32
-42
@@ -1,9 +1,10 @@
|
|||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
|
||||||
module PostgREST.MediaType
|
module PostgREST.MediaType
|
||||||
( MediaType(..)
|
( MediaType(..)
|
||||||
, MTPlanOption (..)
|
, MTVndPlanOption (..)
|
||||||
, MTPlanFormat (..)
|
, MTVndPlanFormat (..)
|
||||||
, toContentType
|
, toContentType
|
||||||
, toMime
|
, toMime
|
||||||
, decodeMediaType
|
, decodeMediaType
|
||||||
@@ -19,8 +20,6 @@ import Protolude
|
|||||||
-- | Enumeration of currently supported media types
|
-- | Enumeration of currently supported media types
|
||||||
data MediaType
|
data MediaType
|
||||||
= MTApplicationJSON
|
= MTApplicationJSON
|
||||||
| MTArrayJSONStrip
|
|
||||||
| MTSingularJSON Bool
|
|
||||||
| MTGeoJSON
|
| MTGeoJSON
|
||||||
| MTTextCSV
|
| MTTextCSV
|
||||||
| MTTextPlain
|
| MTTextPlain
|
||||||
@@ -30,32 +29,23 @@ data MediaType
|
|||||||
| MTOctetStream
|
| MTOctetStream
|
||||||
| MTAny
|
| MTAny
|
||||||
| MTOther ByteString
|
| MTOther ByteString
|
||||||
-- TODO MTPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
|
-- vendored media types
|
||||||
| MTPlan MediaType MTPlanFormat [MTPlanOption]
|
| MTVndArrayJSONStrip
|
||||||
deriving Show
|
| MTVndSingularJSON Bool
|
||||||
instance Eq MediaType where
|
-- TODO MTVndPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
|
||||||
MTApplicationJSON == MTApplicationJSON = True
|
| MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]
|
||||||
MTArrayJSONStrip == MTArrayJSONStrip = True
|
deriving (Eq, Show, Generic)
|
||||||
MTSingularJSON x == MTSingularJSON y = x == y
|
instance Hashable MediaType
|
||||||
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
|
|
||||||
|
|
||||||
data MTPlanOption
|
data MTVndPlanOption
|
||||||
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
|
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show, Generic)
|
||||||
|
instance Hashable MTVndPlanOption
|
||||||
|
|
||||||
data MTPlanFormat
|
data MTVndPlanFormat
|
||||||
= PlanJSON | PlanText
|
= PlanJSON | PlanText
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show, Generic)
|
||||||
|
instance Hashable MTVndPlanFormat
|
||||||
|
|
||||||
-- | Convert MediaType to a Content-Type HTTP Header
|
-- | Convert MediaType to a Content-Type HTTP Header
|
||||||
toContentType :: MediaType -> Header
|
toContentType :: MediaType -> Header
|
||||||
@@ -69,31 +59,31 @@ toContentType ct = (hContentType, toMime ct <> charset)
|
|||||||
-- | Convert from MediaType to a ByteString representing the mime type
|
-- | Convert from MediaType to a ByteString representing the mime type
|
||||||
toMime :: MediaType -> ByteString
|
toMime :: MediaType -> ByteString
|
||||||
toMime MTApplicationJSON = "application/json"
|
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 MTGeoJSON = "application/geo+json"
|
||||||
toMime MTTextCSV = "text/csv"
|
toMime MTTextCSV = "text/csv"
|
||||||
toMime MTTextPlain = "text/plain"
|
toMime MTTextPlain = "text/plain"
|
||||||
toMime MTTextXML = "text/xml"
|
toMime MTTextXML = "text/xml"
|
||||||
toMime MTOpenAPI = "application/openapi+json"
|
toMime MTOpenAPI = "application/openapi+json"
|
||||||
toMime (MTSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
|
toMime (MTVndSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
|
||||||
toMime (MTSingularJSON False) = "application/vnd.pgrst.object+json"
|
toMime (MTVndSingularJSON False) = "application/vnd.pgrst.object+json"
|
||||||
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
|
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
|
||||||
toMime MTOctetStream = "application/octet-stream"
|
toMime MTOctetStream = "application/octet-stream"
|
||||||
toMime MTAny = "*/*"
|
toMime MTAny = "*/*"
|
||||||
toMime (MTOther ct) = ct
|
toMime (MTOther ct) = ct
|
||||||
toMime (MTPlan mt fmt opts) =
|
toMime (MTVndPlan mt fmt opts) =
|
||||||
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
|
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
|
||||||
("; for=\"" <> toMime mt <> "\"") <>
|
("; for=\"" <> toMime mt <> "\"") <>
|
||||||
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
|
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
|
||||||
|
|
||||||
toMimePlanOption :: MTPlanOption -> ByteString
|
toMimePlanOption :: MTVndPlanOption -> ByteString
|
||||||
toMimePlanOption PlanAnalyze = "analyze"
|
toMimePlanOption PlanAnalyze = "analyze"
|
||||||
toMimePlanOption PlanVerbose = "verbose"
|
toMimePlanOption PlanVerbose = "verbose"
|
||||||
toMimePlanOption PlanSettings = "settings"
|
toMimePlanOption PlanSettings = "settings"
|
||||||
toMimePlanOption PlanBuffers = "buffers"
|
toMimePlanOption PlanBuffers = "buffers"
|
||||||
toMimePlanOption PlanWAL = "wal"
|
toMimePlanOption PlanWAL = "wal"
|
||||||
|
|
||||||
toMimePlanFormat :: MTPlanFormat -> ByteString
|
toMimePlanFormat :: MTVndPlanFormat -> ByteString
|
||||||
toMimePlanFormat PlanJSON = "json"
|
toMimePlanFormat PlanJSON = "json"
|
||||||
toMimePlanFormat PlanText = "text"
|
toMimePlanFormat PlanText = "text"
|
||||||
|
|
||||||
@@ -103,25 +93,25 @@ toMimePlanFormat PlanText = "text"
|
|||||||
-- MTApplicationJSON
|
-- MTApplicationJSON
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.plan;"
|
-- >>> decodeMediaType "application/vnd.pgrst.plan;"
|
||||||
-- MTPlan MTApplicationJSON PlanText []
|
-- MTVndPlan MTApplicationJSON PlanText []
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
|
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
|
||||||
-- MTPlan MTApplicationJSON PlanText []
|
-- MTVndPlan MTApplicationJSON PlanText []
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
|
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
|
||||||
-- MTPlan MTTextCSV PlanJSON []
|
-- MTVndPlan MTTextCSV PlanJSON []
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
|
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
|
||||||
-- MTArrayJSONStrip
|
-- MTVndArrayJSONStrip
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
|
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
|
||||||
-- MTApplicationJSON
|
-- MTApplicationJSON
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
|
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
|
||||||
-- MTSingularJSON True
|
-- MTVndSingularJSON True
|
||||||
--
|
--
|
||||||
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
|
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
|
||||||
-- MTSingularJSON False
|
-- MTVndSingularJSON False
|
||||||
|
|
||||||
decodeMediaType :: BS.ByteString -> MediaType
|
decodeMediaType :: BS.ByteString -> MediaType
|
||||||
decodeMediaType mt =
|
decodeMediaType mt =
|
||||||
@@ -145,11 +135,11 @@ decodeMediaType mt =
|
|||||||
other:_ -> MTOther other
|
other:_ -> MTOther other
|
||||||
_ -> MTAny
|
_ -> MTAny
|
||||||
where
|
where
|
||||||
checkArrayNullStrip ["nulls=stripped"] = MTArrayJSONStrip
|
checkArrayNullStrip ["nulls=stripped"] = MTVndArrayJSONStrip
|
||||||
checkArrayNullStrip _ = MTApplicationJSON
|
checkArrayNullStrip _ = MTApplicationJSON
|
||||||
|
|
||||||
checkSingularNullStrip ["nulls=stripped"] = MTSingularJSON True
|
checkSingularNullStrip ["nulls=stripped"] = MTVndSingularJSON True
|
||||||
checkSingularNullStrip _ = MTSingularJSON False
|
checkSingularNullStrip _ = MTVndSingularJSON False
|
||||||
|
|
||||||
getPlan fmt rest =
|
getPlan fmt rest =
|
||||||
let
|
let
|
||||||
@@ -161,7 +151,7 @@ decodeMediaType mt =
|
|||||||
strippedFor <- BS.stripPrefix "for=" foundFor
|
strippedFor <- BS.stripPrefix "for=" foundFor
|
||||||
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor
|
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor
|
||||||
in
|
in
|
||||||
MTPlan mtFor fmt $
|
MTVndPlan mtFor fmt $
|
||||||
[PlanAnalyze | inOpts "analyze" ] ++
|
[PlanAnalyze | inOpts "analyze" ] ++
|
||||||
[PlanVerbose | inOpts "verbose" ] ++
|
[PlanVerbose | inOpts "verbose" ] ++
|
||||||
[PlanSettings | inOpts "settings"] ++
|
[PlanSettings | inOpts "settings"] ++
|
||||||
|
|||||||
+50
-61
@@ -44,8 +44,7 @@ import PostgREST.ApiRequest (Action (..),
|
|||||||
Payload (..))
|
Payload (..))
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.Error (Error (..))
|
import PostgREST.Error (Error (..))
|
||||||
import PostgREST.MediaType (MTPlanFormat (..),
|
import PostgREST.MediaType (MediaType (..))
|
||||||
MediaType (..))
|
|
||||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
convertToLimitZeroRange,
|
convertToLimitZeroRange,
|
||||||
@@ -53,6 +52,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
|||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
|
RelIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
@@ -61,7 +61,7 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
|||||||
relIsToOne)
|
relIsToOne)
|
||||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||||
RepresentationsMap)
|
RepresentationsMap)
|
||||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||||
Routine (..),
|
Routine (..),
|
||||||
RoutineMap,
|
RoutineMap,
|
||||||
RoutineParam (..),
|
RoutineParam (..),
|
||||||
@@ -93,16 +93,18 @@ import Protolude hiding (from)
|
|||||||
data WrappedReadPlan = WrappedReadPlan {
|
data WrappedReadPlan = WrappedReadPlan {
|
||||||
wrReadPlan :: ReadPlanTree
|
wrReadPlan :: ReadPlanTree
|
||||||
, wrTxMode :: SQL.Mode
|
, wrTxMode :: SQL.Mode
|
||||||
, wrResAgg :: ResultAggregate
|
, wrHandler :: MediaHandler
|
||||||
, wrMedia :: MediaType
|
, wrMedia :: MediaType
|
||||||
|
, wrIdent :: QualifiedIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
data MutateReadPlan = MutateReadPlan {
|
data MutateReadPlan = MutateReadPlan {
|
||||||
mrReadPlan :: ReadPlanTree
|
mrReadPlan :: ReadPlanTree
|
||||||
, mrMutatePlan :: MutatePlan
|
, mrMutatePlan :: MutatePlan
|
||||||
, mrTxMode :: SQL.Mode
|
, mrTxMode :: SQL.Mode
|
||||||
, mrResAgg :: ResultAggregate
|
, mrHandler :: MediaHandler
|
||||||
, mrMedia :: MediaType
|
, mrMedia :: MediaType
|
||||||
|
, mrIdent :: QualifiedIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
data CallReadPlan = CallReadPlan {
|
data CallReadPlan = CallReadPlan {
|
||||||
@@ -110,8 +112,9 @@ data CallReadPlan = CallReadPlan {
|
|||||||
, crCallPlan :: CallPlan
|
, crCallPlan :: CallPlan
|
||||||
, crTxMode :: SQL.Mode
|
, crTxMode :: SQL.Mode
|
||||||
, crProc :: Routine
|
, crProc :: Routine
|
||||||
, crResAgg :: ResultAggregate
|
, crHandler :: MediaHandler
|
||||||
, crMedia :: MediaType
|
, crMedia :: MediaType
|
||||||
|
, crIdent :: QualifiedIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
data InspectPlan = InspectPlan {
|
data InspectPlan = InspectPlan {
|
||||||
@@ -122,17 +125,17 @@ data InspectPlan = InspectPlan {
|
|||||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
|
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
|
||||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do
|
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do
|
||||||
rPlan <- readPlan identifier conf sCache apiRequest
|
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 ()
|
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 -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
|
||||||
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
||||||
rPlan <- readPlan identifier conf sCache apiRequest
|
rPlan <- readPlan identifier conf sCache apiRequest
|
||||||
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
|
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 ()
|
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 :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
||||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
|
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.Immutable) -> SQL.Read
|
||||||
(InvPost, Routine.Volatile) -> SQL.Write
|
(InvPost, Routine.Volatile) -> SQL.Write
|
||||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
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 ()
|
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
|
where
|
||||||
qsParams' = QueryParams.qsParams iQueryParams
|
qsParams' = QueryParams.qsParams iQueryParams
|
||||||
|
|
||||||
inspectPlan :: AppConfig -> ApiRequest -> Either Error InspectPlan
|
inspectPlan :: ApiRequest -> Either Error InspectPlan
|
||||||
inspectPlan conf apiRequest = do
|
inspectPlan apiRequest = do
|
||||||
mediaType <- mapLeft ApiRequestError $ negotiateContent conf (iAction apiRequest) (iAcceptMediaType apiRequest)
|
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
|
return $ InspectPlan mediaType SQL.Read
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
@@ -824,52 +831,34 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
|||||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
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.
|
-- | 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 :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] ->
|
||||||
negotiateContent conf action accepts =
|
HM.HashMap (RelIdentifier, MediaType) MediaHandler -> Either ApiRequestError (MediaHandler, MediaType)
|
||||||
case firstAcceptedPick of
|
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces =
|
||||||
Just MTAny -> Right MTApplicationJSON -- by default(for */*) we respond with json
|
mtAnyToJSON $ case (act, firstAcceptedPick) of
|
||||||
Just mt -> Right mt
|
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||||
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
|
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
|
-- if there are multiple accepted media types, pick the first
|
||||||
firstAcceptedPick = listToMaybe $ L.intersect accepts $ producedMediaTypes conf action
|
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
|
||||||
producedMediaTypes :: AppConfig -> Action -> [MediaType]
|
HM.lookup (RelId identifier, mt) produces <|> HM.lookup (RelAnyElement, mt) produces
|
||||||
producedMediaTypes conf action =
|
searchMT mt = case mt of
|
||||||
case action of
|
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||||
ActionRead _ -> defaultMediaTypes
|
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||||
ActionInvoke _ -> defaultMediaTypes
|
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||||
ActionInfo -> defaultMediaTypes
|
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||||
ActionMutate _ -> defaultMediaTypes
|
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||||
ActionInspect _ -> inspectMediaTypes
|
-- all the other media types can be overridden
|
||||||
where
|
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ (,) <$> lookupIdent mType <*> pure m
|
||||||
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
|
x -> (,) <$> lookupIdent x <*> pure x
|
||||||
defaultMediaTypes =
|
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||||
[MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
|
|
||||||
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ readQuery WrappedReadPlan{..} conf@AppConfig{..} apiReq@ApiRequest{iPreferences=
|
|||||||
resultSet <-
|
resultSet <-
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareRead
|
Statements.prepareRead
|
||||||
|
wrIdent
|
||||||
(QueryBuilder.readPlanToQuery wrReadPlan)
|
(QueryBuilder.readPlanToQuery wrReadPlan)
|
||||||
(if preferCount == Just EstimatedCount then
|
(if preferCount == Just EstimatedCount then
|
||||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
-- 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)
|
(shouldCount preferCount)
|
||||||
wrMedia
|
wrMedia
|
||||||
wrResAgg
|
wrHandler
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
failNotSingular wrMedia resultSet
|
failNotSingular wrMedia resultSet
|
||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
@@ -155,13 +156,14 @@ invokeQuery rout CallReadPlan{..} apiReq@ApiRequest{iPreferences=Preferences{..}
|
|||||||
resultSet <-
|
resultSet <-
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareCall
|
Statements.prepareCall
|
||||||
|
crIdent
|
||||||
rout
|
rout
|
||||||
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
|
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
|
||||||
(QueryBuilder.readPlanToQuery crReadPlan)
|
(QueryBuilder.readPlanToQuery crReadPlan)
|
||||||
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
||||||
(shouldCount preferCount)
|
(shouldCount preferCount)
|
||||||
crMedia
|
crMedia
|
||||||
crResAgg
|
crHandler
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
|
|
||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
@@ -186,17 +188,18 @@ openApiQuery sCache pgVer AppConfig{..} tSchema =
|
|||||||
pure Nothing
|
pure Nothing
|
||||||
|
|
||||||
writeQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
writeQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
||||||
writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan, mrResAgg, mrMedia} ApiRequest{iPreferences=Preferences{..}} conf =
|
writeQuery MutateReadPlan{..} ApiRequest{iPreferences=Preferences{..}} conf =
|
||||||
let
|
let
|
||||||
(isInsert, pkCols) = case mrMutatePlan of {Insert{insPkCols} -> (True, insPkCols); _ -> (False, mempty);}
|
(isInsert, pkCols) = case mrMutatePlan of {Insert{insPkCols} -> (True, insPkCols); _ -> (False, mempty);}
|
||||||
in
|
in
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareWrite
|
Statements.prepareWrite
|
||||||
|
mrIdent
|
||||||
(QueryBuilder.readPlanToQuery mrReadPlan)
|
(QueryBuilder.readPlanToQuery mrReadPlan)
|
||||||
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
||||||
isInsert
|
isInsert
|
||||||
mrMedia
|
mrMedia
|
||||||
mrResAgg
|
mrHandler
|
||||||
preferRepresentation
|
preferRepresentation
|
||||||
pkCols
|
pkCols
|
||||||
(configDbPreparedStatements conf)
|
(configDbPreparedStatements conf)
|
||||||
@@ -207,7 +210,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan, mrResAgg, mrMedia} ApiReques
|
|||||||
failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
||||||
failNotSingular _ RSPlan{} = pure ()
|
failNotSingular _ RSPlan{} = pure ()
|
||||||
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
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
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal
|
throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Description : Helper functions for PostgREST.QueryBuilder.
|
|||||||
-}
|
-}
|
||||||
module PostgREST.Query.SqlFragment
|
module PostgREST.Query.SqlFragment
|
||||||
( noLocationF
|
( noLocationF
|
||||||
, aggF
|
, handlerF
|
||||||
, countF
|
, countF
|
||||||
, fromQi
|
, fromQi
|
||||||
, limitOffsetF
|
, limitOffsetF
|
||||||
@@ -65,8 +65,8 @@ import PostgREST.ApiRequest.Types (Alias, Cast,
|
|||||||
QuantOperator (..),
|
QuantOperator (..),
|
||||||
SimpleOperator (..),
|
SimpleOperator (..),
|
||||||
TrileanVal (..))
|
TrileanVal (..))
|
||||||
import PostgREST.MediaType (MTPlanFormat (..),
|
import PostgREST.MediaType (MTVndPlanFormat (..),
|
||||||
MTPlanOption (..))
|
MTVndPlanOption (..))
|
||||||
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
||||||
import PostgREST.Plan.Types (CoercibleField (..),
|
import PostgREST.Plan.Types (CoercibleField (..),
|
||||||
CoercibleFilter (..),
|
CoercibleFilter (..),
|
||||||
@@ -77,7 +77,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
|||||||
rangeLimit, rangeOffset)
|
rangeLimit, rangeOffset)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..))
|
QualifiedIdentifier (..))
|
||||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||||
Routine (..),
|
Routine (..),
|
||||||
funcReturnsScalar,
|
funcReturnsScalar,
|
||||||
funcReturnsSetOfScalar,
|
funcReturnsSetOfScalar,
|
||||||
@@ -212,6 +212,11 @@ asJsonF rout strip
|
|||||||
asGeoJsonF :: SQL.Snippet
|
asGeoJsonF :: SQL.Snippet
|
||||||
asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))"
|
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 :: [Text] -> SQL.Snippet
|
||||||
locationF pKeys = [qc|(
|
locationF pKeys = [qc|(
|
||||||
WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
|
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 _ [] = mempty
|
||||||
intercalateSnippet frag snippets = foldr1 (\a b -> a <> SQL.sql frag <> b) snippets
|
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 =
|
explainF fmt opts snip =
|
||||||
"EXPLAIN (" <>
|
"EXPLAIN (" <>
|
||||||
SQL.sql (BS.intercalate ", " (fmtPlanFmt fmt : (fmtPlanOpt <$> opts))) <>
|
SQL.sql (BS.intercalate ", " (fmtPlanFmt fmt : (fmtPlanOpt <$> opts))) <>
|
||||||
") " <> snip
|
") " <> snip
|
||||||
where
|
where
|
||||||
fmtPlanOpt :: MTPlanOption -> BS.ByteString
|
fmtPlanOpt :: MTVndPlanOption -> BS.ByteString
|
||||||
fmtPlanOpt PlanAnalyze = "ANALYZE"
|
fmtPlanOpt PlanAnalyze = "ANALYZE"
|
||||||
fmtPlanOpt PlanVerbose = "VERBOSE"
|
fmtPlanOpt PlanVerbose = "VERBOSE"
|
||||||
fmtPlanOpt PlanSettings = "SETTINGS"
|
fmtPlanOpt PlanSettings = "SETTINGS"
|
||||||
@@ -486,11 +491,12 @@ setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal k
|
|||||||
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
|
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
|
||||||
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
|
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
|
||||||
|
|
||||||
aggF :: Maybe Routine -> ResultAggregate -> SQL.Snippet
|
handlerF :: Maybe Routine -> QualifiedIdentifier -> MediaHandler -> SQL.Snippet
|
||||||
aggF rout = \case
|
handlerF rout target = \case
|
||||||
BuiltinAggJson -> asJsonF rout False
|
|
||||||
BuiltinAggArrayJsonStrip -> asJsonF rout True
|
BuiltinAggArrayJsonStrip -> asJsonF rout True
|
||||||
BuiltinAggSingleJson strip -> asJsonSingleF rout strip
|
BuiltinAggSingleJson strip -> asJsonSingleF rout strip
|
||||||
BuiltinAggGeoJson -> asGeoJsonF
|
BuiltinOvAggJson -> asJsonF rout False
|
||||||
BuiltinAggCsv -> asCsvF
|
BuiltinOvAggGeoJson -> asGeoJsonF
|
||||||
|
BuiltinOvAggCsv -> asCsvF
|
||||||
|
CustomFunc funcQi -> customFuncF rout funcQi target
|
||||||
NoAgg -> "''::text"
|
NoAgg -> "''::text"
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ import qualified Hasql.Statement as SQL
|
|||||||
import Control.Lens ((^?))
|
import Control.Lens ((^?))
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences
|
import PostgREST.ApiRequest.Preferences
|
||||||
import PostgREST.MediaType (MTPlanFormat (..),
|
import PostgREST.MediaType (MTVndPlanFormat (..),
|
||||||
MediaType (..))
|
MediaType (..))
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier)
|
||||||
Routine, funcReturnsSingle)
|
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine,
|
||||||
|
funcReturnsSingle)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -53,9 +54,9 @@ data ResultSet
|
|||||||
| RSPlan BS.ByteString -- ^ the plan of the query
|
| 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
|
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
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -64,7 +65,7 @@ prepareWrite selectQuery mutateQuery isInsert mt rAgg rep pKeys =
|
|||||||
"'' AS total_result_set, " <>
|
"'' AS total_result_set, " <>
|
||||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||||
locF <> " AS header, " <>
|
locF <> " AS header, " <>
|
||||||
aggF Nothing rAgg <> " AS body, " <>
|
handlerF Nothing qi handler <> " AS body, " <>
|
||||||
responseHeadersF <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF <> " AS response_status " <>
|
responseStatusF <> " AS response_status " <>
|
||||||
"FROM (" <> selectF <> ") _postgrest_t"
|
"FROM (" <> selectF <> ") _postgrest_t"
|
||||||
@@ -80,16 +81,16 @@ prepareWrite selectQuery mutateQuery isInsert mt rAgg rep pKeys =
|
|||||||
|
|
||||||
selectF
|
selectF
|
||||||
-- prevent using any of the column names in ?select= when no response is returned from the CTE
|
-- prevent using any of the column names in ?select= when no response is returned from the CTE
|
||||||
| rAgg == NoAgg = "SELECT * FROM " <> sourceCTE
|
| handler == NoAgg = "SELECT * FROM " <> sourceCTE
|
||||||
| otherwise = selectQuery
|
| otherwise = selectQuery
|
||||||
|
|
||||||
decodeIt :: HD.Result ResultSet
|
decodeIt :: HD.Result ResultSet
|
||||||
decodeIt = case mt of
|
decodeIt = case mt of
|
||||||
MTPlan{} -> planRow
|
MTVndPlan{} -> planRow
|
||||||
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing) <$> HD.rowMaybe (standardRow False)
|
_ -> 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 :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet
|
||||||
prepareRead selectQuery countQuery countTotal mt rAgg =
|
prepareRead qi selectQuery countQuery countTotal mt handler =
|
||||||
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -98,7 +99,7 @@ prepareRead selectQuery countQuery countTotal mt rAgg =
|
|||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
countResultF <> " AS total_result_set, " <>
|
countResultF <> " AS total_result_set, " <>
|
||||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||||
aggF Nothing rAgg <> " AS body, " <>
|
handlerF Nothing qi handler <> " AS body, " <>
|
||||||
responseHeadersF <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF <> " AS response_status " <>
|
responseStatusF <> " AS response_status " <>
|
||||||
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
||||||
@@ -107,13 +108,13 @@ prepareRead selectQuery countQuery countTotal mt rAgg =
|
|||||||
|
|
||||||
decodeIt :: HD.Result ResultSet
|
decodeIt :: HD.Result ResultSet
|
||||||
decodeIt = case mt of
|
decodeIt = case mt of
|
||||||
MTPlan{} -> planRow
|
MTVndPlan{} -> planRow
|
||||||
_ -> HD.singleRow $ standardRow True
|
_ -> HD.singleRow $ standardRow True
|
||||||
|
|
||||||
prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
prepareCall :: QualifiedIdentifier -> Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||||
MediaType -> ResultAggregate -> Bool ->
|
MediaType -> MediaHandler -> Bool ->
|
||||||
SQL.Statement () ResultSet
|
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
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -124,7 +125,7 @@ prepareCall rout callProcQuery selectQuery countQuery countTotal mt rAgg =
|
|||||||
(if funcReturnsSingle rout
|
(if funcReturnsSingle rout
|
||||||
then "1"
|
then "1"
|
||||||
else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <>
|
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, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF <> " AS response_status " <>
|
responseStatusF <> " AS response_status " <>
|
||||||
"FROM (" <> selectQuery <> ") _postgrest_t"
|
"FROM (" <> selectQuery <> ") _postgrest_t"
|
||||||
@@ -133,7 +134,7 @@ prepareCall rout callProcQuery selectQuery countQuery countTotal mt rAgg =
|
|||||||
|
|
||||||
decodeIt :: HD.Result ResultSet
|
decodeIt :: HD.Result ResultSet
|
||||||
decodeIt = case mt of
|
decodeIt = case mt of
|
||||||
MTPlan{} -> planRow
|
MTVndPlan{} -> planRow
|
||||||
_ -> fromMaybe (RSStandard (Just 0) 0 mempty mempty Nothing Nothing) <$> HD.rowMaybe (standardRow True)
|
_ -> fromMaybe (RSStandard (Just 0) 0 mempty mempty Nothing Nothing) <$> HD.rowMaybe (standardRow True)
|
||||||
|
|
||||||
preparePlanRows :: SQL.Snippet -> Bool -> SQL.Statement () (Maybe Int64)
|
preparePlanRows :: SQL.Snippet -> Bool -> SQL.Statement () (Maybe Int64)
|
||||||
@@ -160,8 +161,8 @@ standardRow noLocation =
|
|||||||
|
|
||||||
mtSnippet :: MediaType -> SQL.Snippet -> SQL.Snippet
|
mtSnippet :: MediaType -> SQL.Snippet -> SQL.Snippet
|
||||||
mtSnippet mediaType snippet = case mediaType of
|
mtSnippet mediaType snippet = case mediaType of
|
||||||
MTPlan _ fmt opts -> explainF fmt opts snippet
|
MTVndPlan _ fmt opts -> explainF fmt opts snippet
|
||||||
_ -> snippet
|
_ -> snippet
|
||||||
|
|
||||||
-- | We use rowList because when doing EXPLAIN (FORMAT TEXT), the result comes as many rows. FORMAT JSON comes as one.
|
-- | We use rowList because when doing EXPLAIN (FORMAT TEXT), the result comes as many rows. FORMAT JSON comes as one.
|
||||||
planRow :: HD.Result ResultSet
|
planRow :: HD.Result ResultSet
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
|||||||
& summary .~ pSum
|
& summary .~ pSum
|
||||||
& description .~ mfilter (/="") pDesc
|
& description .~ mfilter (/="") pDesc
|
||||||
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||||
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False]
|
& produces ?~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False]
|
||||||
& at 200 ?~ "OK"
|
& at 200 ?~ "OK"
|
||||||
getOp = procOp
|
getOp = procOp
|
||||||
& parameters .~ makeProcGetParams (pdParams pd)
|
& 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)
|
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||||
& parameters .~ fromList (makeParamDefs ti)
|
& parameters .~ fromList (makeParamDefs ti)
|
||||||
& paths .~ makePathItems pds ti
|
& paths .~ makePathItems pds ti
|
||||||
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
|
& produces .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]
|
||||||
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
|
& consumes .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]
|
||||||
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
|
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
|
||||||
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
|
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ module PostgREST.SchemaCache
|
|||||||
|
|
||||||
import Control.Monad.Extra (whenJust)
|
import Control.Monad.Extra (whenJust)
|
||||||
|
|
||||||
|
import Data.Aeson ((.=))
|
||||||
import qualified Data.Aeson as JSON
|
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 as HM
|
||||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
import qualified Data.HashMap.Strict.InsOrd as HMI
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
@@ -48,7 +50,8 @@ import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
|||||||
pgVersion120)
|
pgVersion120)
|
||||||
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
|
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
RelIdentifier (..),
|
||||||
|
Schema, isAnyElement)
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..),
|
Relationship (..),
|
||||||
@@ -56,6 +59,8 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
|||||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||||
RepresentationsMap)
|
RepresentationsMap)
|
||||||
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
||||||
|
MediaHandler (..),
|
||||||
|
MediaHandlerMap,
|
||||||
PgType (..),
|
PgType (..),
|
||||||
RetType (..),
|
RetType (..),
|
||||||
Routine (..),
|
Routine (..),
|
||||||
@@ -64,6 +69,8 @@ import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
|||||||
import PostgREST.SchemaCache.Table (Column (..), ColumnMap,
|
import PostgREST.SchemaCache.Table (Column (..), ColumnMap,
|
||||||
Table (..), TablesMap)
|
Table (..), TablesMap)
|
||||||
|
|
||||||
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
@@ -72,8 +79,16 @@ data SchemaCache = SchemaCache
|
|||||||
, dbRelationships :: RelationshipsMap
|
, dbRelationships :: RelationshipsMap
|
||||||
, dbRoutines :: RoutineMap
|
, dbRoutines :: RoutineMap
|
||||||
, dbRepresentations :: RepresentationsMap
|
, 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
|
-- | 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.
|
-- 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
|
-- | A SQL query that can be executed independently
|
||||||
type SqlQuery = ByteString
|
type SqlQuery = ByteString
|
||||||
|
|
||||||
|
|
||||||
querySchemaCache :: AppConfig -> SQL.Transaction SchemaCache
|
querySchemaCache :: AppConfig -> SQL.Transaction SchemaCache
|
||||||
querySchemaCache AppConfig{..} = do
|
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
|
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
|
funcs <- SQL.statement schemas $ allFunctions pgVer prepared
|
||||||
cRels <- SQL.statement mempty $ allComputedRels prepared
|
cRels <- SQL.statement mempty $ allComputedRels prepared
|
||||||
reps <- SQL.statement schemas $ dataRepresentations 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
|
let sleepCall = SQL.Statement "select pg_sleep($1)" (param HE.int4) HD.noResult prepared in
|
||||||
whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing
|
whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing
|
||||||
@@ -135,6 +152,7 @@ querySchemaCache AppConfig{..} = do
|
|||||||
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
||||||
, dbRoutines = funcs
|
, dbRoutines = funcs
|
||||||
, dbRepresentations = reps
|
, dbRepresentations = reps
|
||||||
|
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
schemas = toList configDbSchemas
|
schemas = toList configDbSchemas
|
||||||
@@ -169,6 +187,7 @@ removeInternal schemas dbStruct =
|
|||||||
HM.filterWithKey (\(QualifiedIdentifier sch _, _) _ -> sch `elem` schemas ) (dbRelationships 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.
|
, 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
|
, dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API
|
||||||
|
, dbMediaHandlers = dbMediaHandlers dbStruct
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
hasInternalJunction ComputedRelationship{} = False
|
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)
|
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.Value a -> HE.Params a
|
||||||
param = HE.param . HE.nonNullable
|
param = HE.param . HE.nonNullable
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
module PostgREST.SchemaCache.Identifiers
|
module PostgREST.SchemaCache.Identifiers
|
||||||
( QualifiedIdentifier(..)
|
( QualifiedIdentifier(..)
|
||||||
|
, RelIdentifier(..)
|
||||||
|
, isAnyElement
|
||||||
, Schema
|
, Schema
|
||||||
, TableName
|
, TableName
|
||||||
, FieldName
|
, FieldName
|
||||||
@@ -17,6 +19,9 @@ import qualified Data.Text as T
|
|||||||
|
|
||||||
import Protolude
|
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".
|
-- | Represents a pg identifier with a prepended schema name "schema.table".
|
||||||
-- When qiSchema is "", the schema is defined by the pg search_path.
|
-- When qiSchema is "", the schema is defined by the pg search_path.
|
||||||
@@ -28,6 +33,9 @@ data QualifiedIdentifier = QualifiedIdentifier
|
|||||||
|
|
||||||
instance Hashable QualifiedIdentifier
|
instance Hashable QualifiedIdentifier
|
||||||
|
|
||||||
|
isAnyElement :: QualifiedIdentifier -> Bool
|
||||||
|
isAnyElement y = QualifiedIdentifier "pg_catalog" "anyelement" == y
|
||||||
|
|
||||||
dumpQi :: QualifiedIdentifier -> Text
|
dumpQi :: QualifiedIdentifier -> Text
|
||||||
dumpQi (QualifiedIdentifier s i) =
|
dumpQi (QualifiedIdentifier s i) =
|
||||||
(if T.null s then mempty else s <> ".") <> i
|
(if T.null s then mempty else s <> ".") <> i
|
||||||
|
|||||||
@@ -15,16 +15,20 @@ module PostgREST.SchemaCache.Routine
|
|||||||
, funcTableName
|
, funcTableName
|
||||||
, funcReturnsCompositeAlias
|
, funcReturnsCompositeAlias
|
||||||
, funcReturnsSingle
|
, funcReturnsSingle
|
||||||
, ResultAggregate(..)
|
, MediaHandlerMap
|
||||||
|
, MediaHandler(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Aeson ((.=))
|
import Data.Aeson ((.=))
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema, TableName)
|
RelIdentifier (..), Schema,
|
||||||
|
TableName)
|
||||||
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -88,12 +92,17 @@ instance Ord Routine where
|
|||||||
-- | It uses a HashMap for a faster lookup.
|
-- | It uses a HashMap for a faster lookup.
|
||||||
type RoutineMap = HM.HashMap QualifiedIdentifier [Routine]
|
type RoutineMap = HM.HashMap QualifiedIdentifier [Routine]
|
||||||
|
|
||||||
data ResultAggregate
|
-- | A media handler can be an aggregate over a composite type or a function over a scalar
|
||||||
= BuiltinAggJson
|
data MediaHandler
|
||||||
| BuiltinAggSingleJson Bool
|
-- non overridable builtins
|
||||||
|
= BuiltinAggSingleJson Bool
|
||||||
| BuiltinAggArrayJsonStrip
|
| BuiltinAggArrayJsonStrip
|
||||||
| BuiltinAggGeoJson
|
-- these builtins are overridable
|
||||||
| BuiltinAggCsv
|
| BuiltinOvAggJson
|
||||||
|
| BuiltinOvAggGeoJson
|
||||||
|
| BuiltinOvAggCsv
|
||||||
|
-- custom
|
||||||
|
| CustomFunc QualifiedIdentifier
|
||||||
| NoAgg
|
| NoAgg
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
@@ -133,3 +142,5 @@ funcTableName proc = case pdReturnType proc of
|
|||||||
SetOf (Composite qi _) -> Just $ qiName qi
|
SetOf (Composite qi _) -> Just $ qiName qi
|
||||||
Single (Composite qi _) -> Just $ qiName qi
|
Single (Composite qi _) -> Just $ qiName qi
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
type MediaHandlerMap = HM.HashMap (RelIdentifier, MediaType.MediaType) MediaHandler
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ postJsonArrayTest(){
|
|||||||
|
|
||||||
echo "Running memory usage tests.."
|
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" "POST" "/leak?columns=blob" "16M"
|
||||||
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "16M"
|
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "16M"
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ spec :: SpecWith ((), Application)
|
|||||||
spec =
|
spec =
|
||||||
describe "root spec function" $ do
|
describe "root spec function" $ do
|
||||||
it "accepts application/openapi+json" $ do
|
it "accepts application/openapi+json" $ do
|
||||||
pendingWith "TBD"
|
|
||||||
request methodGet "/"
|
request methodGet "/"
|
||||||
[("Accept","application/openapi+json")] "" `shouldRespondWith`
|
[("Accept","application/openapi+json")] "" `shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
@@ -21,3 +20,12 @@ spec =
|
|||||||
"info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}
|
"info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}
|
||||||
}|]
|
}|]
|
||||||
{ matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] }
|
{ 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" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 68.56
|
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
|
describe "text format" $ do
|
||||||
it "outputs the total cost for a function call" $ do
|
it "outputs the total cost for a function call" $ do
|
||||||
r <- request methodGet "/projects?id=in.(1,2,3)"
|
r <- request methodGet "/projects?id=in.(1,2,3)"
|
||||||
@@ -442,6 +430,34 @@ spec actualPgVersion = do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
resBody `shouldSatisfy` (\t -> T.isInfixOf "Index" (decodeUtf8 $ LBS.toStrict t))
|
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 :: SpecWith ((), Application)
|
||||||
disabledSpec =
|
disabledSpec =
|
||||||
it "doesn't work if db-plan-enabled=false(the default)" $ do
|
it "doesn't work if db-plan-enabled=false(the default)" $ do
|
||||||
|
|||||||
@@ -1042,61 +1042,6 @@ spec actualPgVersion = do
|
|||||||
[json|[{"a$num$":100}]|]
|
[json|[{"a$num$":100}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ 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
|
describe "values with quotes in IN and NOT IN" $ do
|
||||||
it "succeeds when only quoted values are present" $ do
|
it "succeeds when only quoted values are present" $ do
|
||||||
get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith`
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
module Feature.Query.RpcSpec where
|
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 (Application)
|
||||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
|
||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
@@ -1077,87 +1076,6 @@ spec actualPgVersion =
|
|||||||
, matchHeaders = []
|
, 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
|
context "only for GET rpc" $ do
|
||||||
it "should fail on mutating procs" $ do
|
it "should fail on mutating procs" $ do
|
||||||
get "/rpc/callcounter" `shouldRespondWith` 405
|
get "/rpc/callcounter" `shouldRespondWith` 405
|
||||||
@@ -1327,7 +1245,6 @@ spec actualPgVersion =
|
|||||||
[json|{"A": 1, "B": 2, "C": 3}|]
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
|
|
||||||
it "can insert text directly" $ do
|
it "can insert text directly" $ do
|
||||||
pendingWith "TBD"
|
|
||||||
request methodPost "/rpc/unnamed_text_param"
|
request methodPost "/rpc/unnamed_text_param"
|
||||||
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
|
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
|
||||||
[str|unnamed text arg|]
|
[str|unnamed text arg|]
|
||||||
@@ -1335,7 +1252,6 @@ spec actualPgVersion =
|
|||||||
[str|unnamed text arg|]
|
[str|unnamed text arg|]
|
||||||
|
|
||||||
it "can insert xml directly" $ do
|
it "can insert xml directly" $ do
|
||||||
pendingWith "TBD"
|
|
||||||
request methodPost "/rpc/unnamed_xml_param"
|
request methodPost "/rpc/unnamed_xml_param"
|
||||||
[("Content-Type", "text/xml"), ("Accept", "text/xml")]
|
[("Content-Type", "text/xml"), ("Accept", "text/xml")]
|
||||||
[str|<note><from>John</from><to>Jane</to><message>Remember me</message></note>|]
|
[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>|]
|
[str|<note><from>John</from><to>Jane</to><message>Remember me</message></note>|]
|
||||||
|
|
||||||
it "can insert bytea directly" $ do
|
it "can insert bytea directly" $ do
|
||||||
pendingWith "TBD"
|
let file = readFixtureFile "image.png"
|
||||||
let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png"
|
|
||||||
r <- request methodPost "/rpc/unnamed_bytea_param"
|
r <- request methodPost "/rpc/unnamed_bytea_param"
|
||||||
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
||||||
file
|
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" $
|
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"
|
request methodPost "/rpc/unnamed_int_param"
|
||||||
[("Content-Type", "application/octet-stream")]
|
[("Content-Type", "application/octet-stream")]
|
||||||
file
|
(readFixtureFile "image.png")
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint": null,
|
"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
|
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"
|
request methodPost "/rpc/overloaded_unnamed_param"
|
||||||
[("Content-Type", "application/json")]
|
[("Content-Type", "application/json")]
|
||||||
[json|{"A": 1, "B": 2, "C": 3}|]
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
@@ -1440,7 +1353,7 @@ spec actualPgVersion =
|
|||||||
[str|unnamed text arg|]
|
[str|unnamed text arg|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[str|unnamed text arg|]
|
[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"
|
r <- request methodPost "/rpc/overloaded_unnamed_param"
|
||||||
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
||||||
file
|
file
|
||||||
@@ -1488,7 +1401,6 @@ spec actualPgVersion =
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "should fail on /rpc/unnamed_xml_param when posting invalid xml" $ do
|
it "should fail on /rpc/unnamed_xml_param when posting invalid xml" $ do
|
||||||
pendingWith "TBD"
|
|
||||||
request methodPost "/rpc/unnamed_xml_param"
|
request methodPost "/rpc/unnamed_xml_param"
|
||||||
[("Content-Type", "text/xml"), ("Accept", "text/xml")]
|
[("Content-Type", "text/xml"), ("Accept", "text/xml")]
|
||||||
[str|<|]
|
[str|<|]
|
||||||
|
|||||||
+2
-2
@@ -37,11 +37,11 @@ import qualified Feature.OpenApi.SecurityOpenApiSpec
|
|||||||
import qualified Feature.OptionsSpec
|
import qualified Feature.OptionsSpec
|
||||||
import qualified Feature.Query.AndOrParamsSpec
|
import qualified Feature.Query.AndOrParamsSpec
|
||||||
import qualified Feature.Query.ComputedRelsSpec
|
import qualified Feature.Query.ComputedRelsSpec
|
||||||
|
import qualified Feature.Query.CustomMediaSpec
|
||||||
import qualified Feature.Query.DeleteSpec
|
import qualified Feature.Query.DeleteSpec
|
||||||
import qualified Feature.Query.EmbedDisambiguationSpec
|
import qualified Feature.Query.EmbedDisambiguationSpec
|
||||||
import qualified Feature.Query.EmbedInnerJoinSpec
|
import qualified Feature.Query.EmbedInnerJoinSpec
|
||||||
import qualified Feature.Query.ErrorSpec
|
import qualified Feature.Query.ErrorSpec
|
||||||
import qualified Feature.Query.HtmlRawOutputSpec
|
|
||||||
import qualified Feature.Query.InsertSpec
|
import qualified Feature.Query.InsertSpec
|
||||||
import qualified Feature.Query.JsonOperatorSpec
|
import qualified Feature.Query.JsonOperatorSpec
|
||||||
import qualified Feature.Query.MultipleSchemaSpec
|
import qualified Feature.Query.MultipleSchemaSpec
|
||||||
@@ -129,6 +129,7 @@ main = do
|
|||||||
, ("Feature.Auth.AuthSpec" , Feature.Auth.AuthSpec.spec actualPgVersion)
|
, ("Feature.Auth.AuthSpec" , Feature.Auth.AuthSpec.spec actualPgVersion)
|
||||||
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
|
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
|
||||||
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
||||||
|
, ("Feature.CustomMediaSpec" , Feature.Query.CustomMediaSpec.spec)
|
||||||
, ("Feature.Query.DeleteSpec" , Feature.Query.DeleteSpec.spec)
|
, ("Feature.Query.DeleteSpec" , Feature.Query.DeleteSpec.spec)
|
||||||
, ("Feature.Query.EmbedDisambiguationSpec" , Feature.Query.EmbedDisambiguationSpec.spec)
|
, ("Feature.Query.EmbedDisambiguationSpec" , Feature.Query.EmbedDisambiguationSpec.spec)
|
||||||
, ("Feature.Query.EmbedInnerJoinSpec" , Feature.Query.EmbedInnerJoinSpec.spec)
|
, ("Feature.Query.EmbedInnerJoinSpec" , Feature.Query.EmbedInnerJoinSpec.spec)
|
||||||
@@ -149,7 +150,6 @@ main = do
|
|||||||
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
|
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
|
||||||
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
||||||
, ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec)
|
, ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec)
|
||||||
, ("Feature.Query.HtmlRawOutputSpec" , Feature.Query.HtmlRawOutputSpec.spec)
|
|
||||||
, ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec)
|
, ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import Data.CaseInsensitive (CI (..), mk, original)
|
|||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.List.NonEmpty (fromList)
|
import Data.List.NonEmpty (fromList)
|
||||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import System.Process (readProcess)
|
import System.Process (readProcess)
|
||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
|
|
||||||
@@ -340,3 +341,6 @@ getInsertDataForTiobePlsTable rows =
|
|||||||
JSON.encode $ fromList $ [TiobePlsRow {name' = nm, rank = rk} | (nm,rk) <- nameRankList]
|
JSON.encode $ fromList $ [TiobePlsRow {name' = nm, rank = rk} | (nm,rk) <- nameRankList]
|
||||||
where
|
where
|
||||||
nameRankList = [("Lang " <> show i, i) | i <- [20..(rows+20)] ] :: [(Text, Int)]
|
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');
|
INSERT INTO table_a(id, name) VALUES (1, 'Not null 1'), (2, null), (3, 'Not null 2');
|
||||||
TRUNCATE TABLE table_b CASCADE;
|
TRUNCATE TABLE table_b CASCADE;
|
||||||
INSERT INTO table_b(table_a_id, name) VALUES (1, 'Test 1'), (2, 'Test 2'), (null, 'Test 3');
|
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'
|
'bar'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create type bit as enum ('one', 'two');
|
||||||
|
|
||||||
SET search_path = postgrest, pg_catalog;
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
@@ -119,6 +120,20 @@ SET default_tablespace = '';
|
|||||||
|
|
||||||
SET default_with_oids = false;
|
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 (
|
CREATE TABLE items (
|
||||||
id bigserial primary key
|
id bigserial primary key
|
||||||
);
|
);
|
||||||
@@ -1160,12 +1175,8 @@ create or replace function test.ret_null() returns int as $$
|
|||||||
select null::int;
|
select null::int;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create function test.ret_base64_bin() returns text as $$
|
create function test.ret_image() returns "image/png" as $$
|
||||||
select i.img from test.images_base64 i where i.name = 'A.png';
|
select i.img::"image/png" from test.images 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;
|
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create function test.single_article(id integer) returns test.articles as $$
|
create function test.single_article(id integer) returns test.articles as $$
|
||||||
@@ -1896,7 +1907,7 @@ returns integer as $$
|
|||||||
select a + b;
|
select a + b;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function root() returns json as $_$
|
create or replace function root() returns "application/openapi+json" as $_$
|
||||||
declare
|
declare
|
||||||
openapi json = $$
|
openapi json = $$
|
||||||
{
|
{
|
||||||
@@ -1912,17 +1923,17 @@ begin
|
|||||||
end
|
end
|
||||||
$_$ language plpgsql;
|
$_$ language plpgsql;
|
||||||
|
|
||||||
create or replace function welcome() returns text as $$
|
create or replace function welcome() returns "text/plain" as $$
|
||||||
select 'Welcome to PostgREST'::text;
|
select 'Welcome to PostgREST'::"text/plain";
|
||||||
$$ language sql;
|
$$ 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'
|
select 'Welcome to PostgREST'
|
||||||
union all
|
union all
|
||||||
select 'Welcome to PostgREST';
|
select 'Welcome to PostgREST';
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function "welcome.html"() returns text as $_$
|
create or replace function "welcome.html"() returns "text/html" as $_$
|
||||||
select $$
|
select $$
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -1932,7 +1943,7 @@ select $$
|
|||||||
<h1>Welcome to PostgREST</h1>
|
<h1>Welcome to PostgREST</h1>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
$$::text;
|
$$::"text/html";
|
||||||
$_$ language sql;
|
$_$ language sql;
|
||||||
|
|
||||||
create view getallprojects_view as
|
create view getallprojects_view as
|
||||||
@@ -2370,16 +2381,16 @@ create or replace function test.unnamed_json_param(json) returns json as $$
|
|||||||
select $1;
|
select $1;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.unnamed_text_param(text) returns text as $$
|
create or replace function test.unnamed_text_param(text) returns "text/plain" as $$
|
||||||
select $1;
|
select $1::"text/plain";
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.unnamed_xml_param(pg_catalog.xml) returns pg_catalog.xml as $$
|
create or replace function test.unnamed_xml_param(pg_catalog.xml) returns "text/xml" as $$
|
||||||
select $1;
|
select $1::"text/xml";
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.unnamed_bytea_param(bytea) returns bytea as $$
|
create or replace function test.unnamed_bytea_param(bytea) returns "application/octet-stream" as $$
|
||||||
select $1::bytea;
|
select $1::"application/octet-stream";
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.unnamed_int_param(int) returns int as $$
|
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;
|
select $1;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.overloaded_unnamed_param(bytea) returns bytea as $$
|
create or replace function test.overloaded_unnamed_param(bytea) returns "application/octet-stream" as $$
|
||||||
select $1;
|
select $1::"application/octet-stream";
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.overloaded_unnamed_param(text) returns text as $$
|
create or replace function test.overloaded_unnamed_param(text) returns "text/plain" as $$
|
||||||
select $1;
|
select $1::"text/plain";
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.overloaded_unnamed_param() returns int as $$
|
create or replace function test.overloaded_unnamed_param() returns int as $$
|
||||||
@@ -2631,12 +2642,12 @@ create table plate_plan_step (
|
|||||||
REFERENCES well(well_id)
|
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 $$
|
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 $_$
|
LANGUAGE sql AS $_$
|
||||||
select $$
|
select $$
|
||||||
<html>
|
<html>
|
||||||
@@ -2646,7 +2657,7 @@ select $$
|
|||||||
<body>
|
<body>
|
||||||
<h1>Welcome to PostgREST</h1>
|
<h1>Welcome to PostgREST</h1>
|
||||||
</body>
|
</body>
|
||||||
</html>$$::pg_catalog.xml;
|
</html>$$::"text/xml";
|
||||||
$_$;
|
$_$;
|
||||||
|
|
||||||
CREATE TABLE test.xmltest (
|
CREATE TABLE test.xmltest (
|
||||||
@@ -2654,6 +2665,23 @@ CREATE TABLE test.xmltest (
|
|||||||
xml pg_catalog.xml NOT NULL
|
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(
|
CREATE TABLE oid_test(
|
||||||
id int,
|
id int,
|
||||||
oid_col oid,
|
oid_col oid,
|
||||||
@@ -3471,3 +3499,160 @@ stable
|
|||||||
as $$ begin
|
as $$ begin
|
||||||
return query select items2.id from items2 where items2.id=search2.id;
|
return query select items2.id from items2 where items2.id=search2.id;
|
||||||
end$$;
|
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