feat: add optional nulls=stripped parameter for mediatypes applicatio… (#2894)

This commit is contained in:
Taimoor Zaeem
2023-08-09 18:49:22 -05:00
committed by GitHub
parent d490bf09fd
commit fbf9bf21c2
13 changed files with 227 additions and 68 deletions
+1
View File
@@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2856, Add the `--version` CLI option that prints the version information - @laurenceisla - #2856, Add the `--version` CLI option that prints the version information - @laurenceisla
- #1655, Improve `details` field of the singular error response - @taimoorzaeem - #1655, Improve `details` field of the singular error response - @taimoorzaeem
- #740, Add `Preference-Applied` in response for `Prefer: return=representation/headers-only/minimal` - @taimoorzaeem - #740, Add `Preference-Applied` in response for `Prefer: return=representation/headers-only/minimal` - @taimoorzaeem
- #1601, Add optional `nulls=stripped` parameter for mediatypes `application/vnd.pgrst.array+json` and `application/vnd.pgrst.object+json` - @taimoorzaeem
### Fixed ### Fixed
+1
View File
@@ -218,6 +218,7 @@ test-suite spec
Feature.Query.RelatedQueriesSpec Feature.Query.RelatedQueriesSpec
Feature.Query.RpcSpec Feature.Query.RpcSpec
Feature.Query.SingularSpec Feature.Query.SingularSpec
Feature.Query.NullsStrip
Feature.Query.SpreadQueriesSpec Feature.Query.SpreadQueriesSpec
Feature.Query.UnicodeSpec Feature.Query.UnicodeSpec
Feature.Query.UpdateSpec Feature.Query.UpdateSpec
+3 -2
View File
@@ -366,13 +366,14 @@ producedMediaTypes conf action path =
ActionInvoke _ -> invokeMediaTypes ActionInvoke _ -> invokeMediaTypes
ActionInfo -> defaultMediaTypes ActionInfo -> defaultMediaTypes
ActionMutate _ -> defaultMediaTypes ActionMutate _ -> defaultMediaTypes
ActionInspect _ -> [MTOpenAPI, MTApplicationJSON, MTAny] ActionInspect _ -> inspectMediaTypes
where where
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
invokeMediaTypes = invokeMediaTypes =
defaultMediaTypes defaultMediaTypes
++ rawMediaTypes ++ rawMediaTypes
++ [MTOpenAPI | pathIsRootSpec path] ++ [MTOpenAPI | pathIsRootSpec path]
defaultMediaTypes = defaultMediaTypes =
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV] ++ [MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny] [MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML] rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
+1 -1
View File
@@ -478,7 +478,7 @@ instance PgrstError Error where
headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m] headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m]
headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader] headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader]
headers (PgErr err) = headers err headers (PgErr err) = headers err
headers SingularityError{} = [MediaType.toContentType MTSingularJSON] headers SingularityError{} = [MediaType.toContentType (MTSingularJSON False)]
headers _ = [MediaType.toContentType MTApplicationJSON] headers _ = [MediaType.toContentType MTApplicationJSON]
instance JSON.ToJSON Error where instance JSON.ToJSON Error where
+66 -42
View File
@@ -19,7 +19,8 @@ import Protolude
-- | Enumeration of currently supported media types -- | Enumeration of currently supported media types
data MediaType data MediaType
= MTApplicationJSON = MTApplicationJSON
| MTSingularJSON | MTArrayJSONStrip
| MTSingularJSON Bool
| MTGeoJSON | MTGeoJSON
| MTTextCSV | MTTextCSV
| MTTextPlain | MTTextPlain
@@ -33,19 +34,20 @@ data MediaType
| MTPlan MediaType MTPlanFormat [MTPlanOption] | MTPlan MediaType MTPlanFormat [MTPlanOption]
deriving Show deriving Show
instance Eq MediaType where instance Eq MediaType where
MTApplicationJSON == MTApplicationJSON = True MTApplicationJSON == MTApplicationJSON = True
MTSingularJSON == MTSingularJSON = True MTArrayJSONStrip == MTArrayJSONStrip = True
MTGeoJSON == MTGeoJSON = True MTSingularJSON x == MTSingularJSON y = x == y
MTTextCSV == MTTextCSV = True MTGeoJSON == MTGeoJSON = True
MTTextPlain == MTTextPlain = True MTTextCSV == MTTextCSV = True
MTTextXML == MTTextXML = True MTTextPlain == MTTextPlain = True
MTOpenAPI == MTOpenAPI = True MTTextXML == MTTextXML = True
MTUrlEncoded == MTUrlEncoded = True MTOpenAPI == MTOpenAPI = True
MTOctetStream == MTOctetStream = True MTUrlEncoded == MTUrlEncoded = True
MTAny == MTAny = True MTOctetStream == MTOctetStream = True
MTOther x == MTOther y = x == y MTAny == MTAny = True
MTPlan{} == MTPlan{} = True MTOther x == MTOther y = x == y
_ == _ = False MTPlan{} == MTPlan{} = True
_ == _ = False
data MTPlanOption data MTPlanOption
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL = PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
@@ -66,18 +68,20 @@ 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 MTGeoJSON = "application/geo+json" toMime MTArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
toMime MTTextCSV = "text/csv" toMime MTGeoJSON = "application/geo+json"
toMime MTTextPlain = "text/plain" toMime MTTextCSV = "text/csv"
toMime MTTextXML = "text/xml" toMime MTTextPlain = "text/plain"
toMime MTOpenAPI = "application/openapi+json" toMime MTTextXML = "text/xml"
toMime MTSingularJSON = "application/vnd.pgrst.object+json" toMime MTOpenAPI = "application/openapi+json"
toMime MTUrlEncoded = "application/x-www-form-urlencoded" toMime (MTSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
toMime MTOctetStream = "application/octet-stream" toMime (MTSingularJSON False) = "application/vnd.pgrst.object+json"
toMime MTAny = "*/*" toMime MTUrlEncoded = "application/x-www-form-urlencoded"
toMime (MTOther ct) = ct toMime MTOctetStream = "application/octet-stream"
toMime (MTPlan mt fmt opts) = toMime MTAny = "*/*"
toMime (MTOther ct) = ct
toMime (MTPlan 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))
@@ -106,26 +110,46 @@ toMimePlanFormat PlanText = "text"
-- --
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\"" -- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- MTPlan MTTextCSV PlanJSON [] -- MTPlan MTTextCSV PlanJSON []
--
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
-- MTArrayJSONStrip
--
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
-- MTApplicationJSON
--
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
-- MTSingularJSON True
--
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
-- MTSingularJSON False
decodeMediaType :: BS.ByteString -> MediaType decodeMediaType :: BS.ByteString -> MediaType
decodeMediaType mt = decodeMediaType mt =
case BS.split (BS.c2w ';') mt of case BS.split (BS.c2w ';') mt of
"application/json":_ -> MTApplicationJSON "application/json":_ -> MTApplicationJSON
"application/geo+json":_ -> MTGeoJSON "application/geo+json":_ -> MTGeoJSON
"text/csv":_ -> MTTextCSV "text/csv":_ -> MTTextCSV
"text/plain":_ -> MTTextPlain "text/plain":_ -> MTTextPlain
"text/xml":_ -> MTTextXML "text/xml":_ -> MTTextXML
"application/openapi+json":_ -> MTOpenAPI "application/openapi+json":_ -> MTOpenAPI
"application/vnd.pgrst.object+json":_ -> MTSingularJSON "application/x-www-form-urlencoded":_ -> MTUrlEncoded
"application/vnd.pgrst.object":_ -> MTSingularJSON "application/octet-stream":_ -> MTOctetStream
"application/x-www-form-urlencoded":_ -> MTUrlEncoded "application/vnd.pgrst.plan":rest -> getPlan PlanText rest
"application/octet-stream":_ -> MTOctetStream "application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest
"application/vnd.pgrst.plan":rest -> getPlan PlanText rest "application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest
"application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest "application/vnd.pgrst.object+json":rest -> checkSingularNullStrip rest
"application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest "application/vnd.pgrst.object":rest -> checkSingularNullStrip rest
"*/*":_ -> MTAny "application/vnd.pgrst.array+json":rest -> checkArrayNullStrip rest
other:_ -> MTOther other "*/*":_ -> MTAny
_ -> MTAny other:_ -> MTOther other
_ -> MTAny
where where
checkArrayNullStrip ["nulls=stripped"] = MTArrayJSONStrip
checkArrayNullStrip _ = MTApplicationJSON
checkSingularNullStrip ["nulls=stripped"] = MTSingularJSON True
checkSingularNullStrip _ = MTSingularJSON False
getPlan fmt rest = getPlan fmt rest =
let let
opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest) opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest)
+2 -1
View File
@@ -847,7 +847,8 @@ mediaToAggregate mt binField apiReq@ApiRequest{iAction=act, iPreferences=Prefere
if noAgg then NoAgg if noAgg then NoAgg
else case mt of else case mt of
MTApplicationJSON -> BuiltinAggJson MTApplicationJSON -> BuiltinAggJson
MTSingularJSON -> BuiltinAggSingleJson MTSingularJSON strip -> BuiltinAggSingleJson strip
MTArrayJSONStrip -> BuiltinAggArrayJsonStrip
MTGeoJSON -> BuiltinAggGeoJson MTGeoJSON -> BuiltinAggGeoJson
MTTextCSV -> BuiltinAggCsv MTTextCSV -> BuiltinAggCsv
MTAny -> BuiltinAggJson MTAny -> BuiltinAggJson
+1 -1
View File
@@ -206,7 +206,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan, mrResAgg} apiReq@ApiRequest{
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 (mediaType == MTSingularJSON && queryTotal /= 1) $ do when (elem mediaType [MTSingularJSON True,MTSingularJSON False] && queryTotal /= 1) $ do
lift SQL.condemn lift SQL.condemn
throwError $ Error.singularityError queryTotal throwError $ Error.singularityError queryTotal
+23 -17
View File
@@ -186,24 +186,29 @@ asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
")" ")"
asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')" asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
asJsonSingleF :: Maybe Routine -> SQL.Snippet addNullsToSnip :: Bool -> SQL.Snippet -> SQL.Snippet
asJsonSingleF rout addNullsToSnip strip snip =
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')" if strip then "json_strip_nulls(" <> snip <> ")" else snip
| otherwise = "coalesce(json_agg(_postgrest_t)->0, 'null')"
asJsonSingleF :: Maybe Routine -> Bool -> SQL.Snippet
asJsonSingleF rout strip
| returnsScalar = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t.pgrst_scalar)->0" <> ", 'null')"
| otherwise = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t)->0" <> ", 'null')"
where where
returnsScalar = maybe False funcReturnsScalar rout returnsScalar = maybe False funcReturnsScalar rout
asJsonF :: Maybe Routine -> SQL.Snippet asJsonF :: Maybe Routine -> Bool -> SQL.Snippet
asJsonF rout asJsonF rout strip
| returnsSingleComposite = "coalesce(json_agg(_postgrest_t)->0, 'null')" | returnsSingleComposite = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t)->0" <> ", 'null')"
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')" | returnsScalar = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t.pgrst_scalar)->0" <> ", 'null')"
| returnsSetOfScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar), '[]')" | returnsSetOfScalar = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t.pgrst_scalar)" <> ", '[]')"
| otherwise = "coalesce(json_agg(_postgrest_t), '[]')" | otherwise = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t)" <> ", '[]')"
where where
(returnsSingleComposite, returnsScalar, returnsSetOfScalar) = case rout of (returnsSingleComposite, returnsScalar, returnsSetOfScalar) = case rout of
Just r -> (funcReturnsSingleComposite r, funcReturnsScalar r, funcReturnsSetOfScalar r) Just r -> (funcReturnsSingleComposite r, funcReturnsScalar r, funcReturnsSetOfScalar r)
Nothing -> (False, False, False) Nothing -> (False, False, False)
asXmlF :: Maybe FieldName -> SQL.Snippet asXmlF :: Maybe FieldName -> SQL.Snippet
asXmlF (Just fieldName) = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')" asXmlF (Just fieldName) = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
-- TODO unreachable because a previous step(binaryField) will validate that there's a field. This will be cleared once custom media types are implemented. -- TODO unreachable because a previous step(binaryField) will validate that there's a field. This will be cleared once custom media types are implemented.
@@ -493,10 +498,11 @@ setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal k
aggF :: Maybe Routine -> ResultAggregate -> SQL.Snippet aggF :: Maybe Routine -> ResultAggregate -> SQL.Snippet
aggF rout = \case aggF rout = \case
BuiltinAggJson -> asJsonF rout BuiltinAggJson -> asJsonF rout False
BuiltinAggSingleJson -> asJsonSingleF rout BuiltinAggArrayJsonStrip -> asJsonF rout True
BuiltinAggGeoJson -> asGeoJsonF BuiltinAggSingleJson strip -> asJsonSingleF rout strip
BuiltinAggCsv -> asCsvF BuiltinAggGeoJson -> asGeoJsonF
BuiltinAggXml bField -> asXmlF bField BuiltinAggCsv -> asCsvF
BuiltinAggBinary bField -> asBinaryF bField BuiltinAggXml bField -> asXmlF bField
NoAgg -> "''::text" BuiltinAggBinary bField -> asBinaryF bField
NoAgg -> "''::text"
+3 -3
View File
@@ -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] & produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON 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, MTTextCSV] & produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV] & consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef & securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef] & security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
where where
+2 -1
View File
@@ -90,7 +90,8 @@ type RoutineMap = HM.HashMap QualifiedIdentifier [Routine]
data ResultAggregate data ResultAggregate
= BuiltinAggJson = BuiltinAggJson
| BuiltinAggSingleJson | BuiltinAggSingleJson Bool
| BuiltinAggArrayJsonStrip
| BuiltinAggGeoJson | BuiltinAggGeoJson
| BuiltinAggCsv | BuiltinAggCsv
| BuiltinAggXml (Maybe FieldName) | BuiltinAggXml (Maybe FieldName)
+116
View File
@@ -0,0 +1,116 @@
module Feature.Query.NullsStrip where
import Network.Wai (Application)
import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application)
spec =
describe "Stripping null values from JSON response" $ do
let arrayStrip = ("Accept", "application/vnd.pgrst.array+json;nulls=stripped")
let singularStrip = ("Accept", "application/vnd.pgrst.object+json;nulls=stripped")
context "strip nulls from response" $ do
it "strips nulls when Accept: application/vnd.pgrst.array+json;nulls=stripped" $ do
request methodGet "/organizations?select=*"
[arrayStrip]
""
`shouldRespondWith`
[json|[{"id":1,"name":"Referee Org","manager_id":1},{"id":2,"name":"Auditor Org","manager_id":2},{"id":3,"name":"Acme","referee":1,"auditor":2,"manager_id":3},{"id":4,"name":"Umbrella","referee":1,"auditor":2,"manager_id":4},{"id":5,"name":"Cyberdyne","referee":3,"auditor":4,"manager_id":5},{"id":6,"name":"Oscorp","referee":3,"auditor":4,"manager_id":6}]|]
{ matchStatus = 200
, matchHeaders = [matchCTArrayStrip]
}
request methodPost "/organizations?select=*"
[arrayStrip,("Prefer","return=representation")]
[json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|]
`shouldRespondWith`
[json|[{"id":7,"name":"John","manager_id":6}]|]
{ matchStatus = 201
, matchHeaders = [matchCTArrayStrip]
}
request methodPatch "/organizations?id=eq.3&select=*"
[arrayStrip, ("Prefer","return=representation")]
[json|{"name":"John","referee":null}|]
`shouldRespondWith`
[json|[{"id":3,"name":"John","auditor":2,"manager_id":3}]|]
{ matchStatus = 200
, matchHeaders = [matchCTArrayStrip]
}
it "strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped" $
request methodGet "/organizations?limit=1"
[singularStrip]
""
`shouldRespondWith`
[json|{"id":1,"name":"Referee Org","manager_id":1}|]
{ matchStatus = 200
, matchHeaders = [matchCTSingularStrip]
}
it "throws error when Accept: application/vnd.pgrst.object+json;nulls=stripped and result not singular" $
request methodGet "/organizations?select=*"
[singularStrip]
""
`shouldRespondWith`
[json|{"details":"The result contains 6 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
context "strip nulls from response even if explicitly selected" $ do
it "strips nulls when Accept: application/vnd.pgrst.array+json;nulls=stripped" $ do
request methodGet "/organizations?select=id,referee,auditor"
[arrayStrip]
""
`shouldRespondWith`
[json|[{"id":1},{"id":2},{"id":3,"referee":1,"auditor":2},{"id":4,"referee":1,"auditor":2},{"id":5,"referee":3,"auditor":4},{"id":6,"referee":3,"auditor":4}]|]
{ matchStatus = 200
, matchHeaders = [matchCTArrayStrip]
}
request methodPost "/organizations?select=id,referee,auditor"
[arrayStrip,("Prefer","return=representation")]
[json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|]
`shouldRespondWith`
[json|[{"id":7}]|]
{ matchStatus = 201
, matchHeaders = [matchCTArrayStrip]
}
request methodPatch "/organizations?id=eq.3&select=id,name,referee,auditor"
[arrayStrip, ("Prefer","return=representation")]
[json|{"name":"John","referee":null}|]
`shouldRespondWith`
[json|[{"id":3,"name":"John","auditor":2}]|]
{ matchStatus = 200
, matchHeaders = [matchCTArrayStrip]
}
it "strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped" $
request methodGet "/organizations?select=id,referee,auditor&limit=1"
[singularStrip]
""
`shouldRespondWith`
[json|{"id":1}|]
{ matchStatus = 200
, matchHeaders = [matchCTSingularStrip]
}
context "doesn't strip nulls" $ do
it "doesn't strips nulls when Accept: application/vnd.pgrst.array+json" $
request methodGet "/organizations?select=id,referee,auditor"
[("Accept", "application/vnd.pgrst.array+json")]
""
`shouldRespondWith`
[json|[{"id":1,"referee":null,"auditor":null},{"id":2,"referee":null,"auditor":null},{"id":3,"referee":1,"auditor":2},{"id":4,"referee":1,"auditor":2},{"id":5,"referee":3,"auditor":4},{"id":6,"referee":3,"auditor":4}]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
+2
View File
@@ -45,6 +45,7 @@ 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
import qualified Feature.Query.NullsStrip
import qualified Feature.Query.PgSafeUpdateSpec import qualified Feature.Query.PgSafeUpdateSpec
import qualified Feature.Query.PlanSpec import qualified Feature.Query.PlanSpec
import qualified Feature.Query.PostGISSpec import qualified Feature.Query.PostGISSpec
@@ -139,6 +140,7 @@ main = do
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec) , ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
, ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion) , ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion)
, ("Feature.Query.SingularSpec" , Feature.Query.SingularSpec.spec) , ("Feature.Query.SingularSpec" , Feature.Query.SingularSpec.spec)
, ("Feature.Query.NullsStrip" , Feature.Query.NullsStrip.spec)
, ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec actualPgVersion) , ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec actualPgVersion)
, ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion) , ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion)
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec) , ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
+6
View File
@@ -40,6 +40,12 @@ matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"
matchContentTypeSingular :: MatchHeader matchContentTypeSingular :: MatchHeader
matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8" matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"
matchCTArrayStrip :: MatchHeader
matchCTArrayStrip = "Content-Type" <:> "application/vnd.pgrst.array+json;nulls=stripped; charset=utf-8"
matchCTSingularStrip :: MatchHeader
matchCTSingularStrip = "Content-Type" <:> "application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8"
matchHeaderAbsent :: HeaderName -> MatchHeader matchHeaderAbsent :: HeaderName -> MatchHeader
matchHeaderAbsent name = MatchHeader $ \headers _body -> matchHeaderAbsent name = MatchHeader $ \headers _body ->
case lookup name headers of case lookup name headers of