Skip to content

Commit aaeb1b7

Browse files
committed
feat: add optional nulls=stripped parameter for mediatypes application/vnd.pgrst.array+json and application/vnd.pgrst.object+json
1 parent d490bf0 commit aaeb1b7

File tree

13 files changed

+181
-68
lines changed

13 files changed

+181
-68
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2424
- #2856, Add the `--version` CLI option that prints the version information - @laurenceisla
2525
- #1655, Improve `details` field of the singular error response - @taimoorzaeem
2626
- #740, Add `Preference-Applied` in response for `Prefer: return=representation/headers-only/minimal` - @taimoorzaeem
27+
- #1601, Add optional `nulls=stripped` parameter for mediatypes `application/vnd.pgrst.array+json` and `application/vnd.pgrst.object+json` - @taimoorzaeem
2728

2829
### Fixed
2930

postgrest.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ test-suite spec
218218
Feature.Query.RelatedQueriesSpec
219219
Feature.Query.RpcSpec
220220
Feature.Query.SingularSpec
221+
Feature.Query.NullsStrip
221222
Feature.Query.SpreadQueriesSpec
222223
Feature.Query.UnicodeSpec
223224
Feature.Query.UpdateSpec

src/PostgREST/ApiRequest.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,14 @@ producedMediaTypes conf action path =
366366
ActionInvoke _ -> invokeMediaTypes
367367
ActionInfo -> defaultMediaTypes
368368
ActionMutate _ -> defaultMediaTypes
369-
ActionInspect _ -> [MTOpenAPI, MTApplicationJSON, MTAny]
369+
ActionInspect _ -> inspectMediaTypes
370370
where
371+
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
371372
invokeMediaTypes =
372373
defaultMediaTypes
373374
++ rawMediaTypes
374375
++ [MTOpenAPI | pathIsRootSpec path]
375376
defaultMediaTypes =
376-
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV] ++
377+
[MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
377378
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
378379
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]

src/PostgREST/Error.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ instance PgrstError Error where
478478
headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m]
479479
headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader]
480480
headers (PgErr err) = headers err
481-
headers SingularityError{} = [MediaType.toContentType MTSingularJSON]
481+
headers SingularityError{} = [MediaType.toContentType (MTSingularJSON False)]
482482
headers _ = [MediaType.toContentType MTApplicationJSON]
483483

484484
instance JSON.ToJSON Error where

src/PostgREST/MediaType.hs

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import Protolude
1919
-- | Enumeration of currently supported media types
2020
data MediaType
2121
= MTApplicationJSON
22-
| MTSingularJSON
22+
| MTArrayJSONStrip
23+
| MTSingularJSON Bool
2324
| MTGeoJSON
2425
| MTTextCSV
2526
| MTTextPlain
@@ -33,19 +34,20 @@ data MediaType
3334
| MTPlan MediaType MTPlanFormat [MTPlanOption]
3435
deriving Show
3536
instance Eq MediaType where
36-
MTApplicationJSON == MTApplicationJSON = True
37-
MTSingularJSON == MTSingularJSON = True
38-
MTGeoJSON == MTGeoJSON = True
39-
MTTextCSV == MTTextCSV = True
40-
MTTextPlain == MTTextPlain = True
41-
MTTextXML == MTTextXML = True
42-
MTOpenAPI == MTOpenAPI = True
43-
MTUrlEncoded == MTUrlEncoded = True
44-
MTOctetStream == MTOctetStream = True
45-
MTAny == MTAny = True
46-
MTOther x == MTOther y = x == y
47-
MTPlan{} == MTPlan{} = True
48-
_ == _ = False
37+
MTApplicationJSON == MTApplicationJSON = True
38+
MTArrayJSONStrip == MTArrayJSONStrip = True
39+
MTSingularJSON x == MTSingularJSON y = x == y
40+
MTGeoJSON == MTGeoJSON = True
41+
MTTextCSV == MTTextCSV = True
42+
MTTextPlain == MTTextPlain = True
43+
MTTextXML == MTTextXML = True
44+
MTOpenAPI == MTOpenAPI = True
45+
MTUrlEncoded == MTUrlEncoded = True
46+
MTOctetStream == MTOctetStream = True
47+
MTAny == MTAny = True
48+
MTOther x == MTOther y = x == y
49+
MTPlan{} == MTPlan{} = True
50+
_ == _ = False
4951

5052
data MTPlanOption
5153
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
@@ -66,18 +68,20 @@ toContentType ct = (hContentType, toMime ct <> charset)
6668

6769
-- | Convert from MediaType to a ByteString representing the mime type
6870
toMime :: MediaType -> ByteString
69-
toMime MTApplicationJSON = "application/json"
70-
toMime MTGeoJSON = "application/geo+json"
71-
toMime MTTextCSV = "text/csv"
72-
toMime MTTextPlain = "text/plain"
73-
toMime MTTextXML = "text/xml"
74-
toMime MTOpenAPI = "application/openapi+json"
75-
toMime MTSingularJSON = "application/vnd.pgrst.object+json"
76-
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
77-
toMime MTOctetStream = "application/octet-stream"
78-
toMime MTAny = "*/*"
79-
toMime (MTOther ct) = ct
80-
toMime (MTPlan mt fmt opts) =
71+
toMime MTApplicationJSON = "application/json"
72+
toMime MTArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
73+
toMime MTGeoJSON = "application/geo+json"
74+
toMime MTTextCSV = "text/csv"
75+
toMime MTTextPlain = "text/plain"
76+
toMime MTTextXML = "text/xml"
77+
toMime MTOpenAPI = "application/openapi+json"
78+
toMime (MTSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
79+
toMime (MTSingularJSON False) = "application/vnd.pgrst.object+json"
80+
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
81+
toMime MTOctetStream = "application/octet-stream"
82+
toMime MTAny = "*/*"
83+
toMime (MTOther ct) = ct
84+
toMime (MTPlan mt fmt opts) =
8185
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
8286
("; for=\"" <> toMime mt <> "\"") <>
8387
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
@@ -106,26 +110,46 @@ toMimePlanFormat PlanText = "text"
106110
--
107111
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
108112
-- MTPlan MTTextCSV PlanJSON []
113+
--
114+
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
115+
-- MTArrayJSONStrip
116+
--
117+
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
118+
-- MTApplicationJSON
119+
--
120+
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
121+
-- MTSingularJSON True
122+
--
123+
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
124+
-- MTSingularJSON False
125+
109126
decodeMediaType :: BS.ByteString -> MediaType
110127
decodeMediaType mt =
111128
case BS.split (BS.c2w ';') mt of
112-
"application/json":_ -> MTApplicationJSON
113-
"application/geo+json":_ -> MTGeoJSON
114-
"text/csv":_ -> MTTextCSV
115-
"text/plain":_ -> MTTextPlain
116-
"text/xml":_ -> MTTextXML
117-
"application/openapi+json":_ -> MTOpenAPI
118-
"application/vnd.pgrst.object+json":_ -> MTSingularJSON
119-
"application/vnd.pgrst.object":_ -> MTSingularJSON
120-
"application/x-www-form-urlencoded":_ -> MTUrlEncoded
121-
"application/octet-stream":_ -> MTOctetStream
122-
"application/vnd.pgrst.plan":rest -> getPlan PlanText rest
123-
"application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest
124-
"application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest
125-
"*/*":_ -> MTAny
126-
other:_ -> MTOther other
127-
_ -> MTAny
129+
"application/json":_ -> MTApplicationJSON
130+
"application/geo+json":_ -> MTGeoJSON
131+
"text/csv":_ -> MTTextCSV
132+
"text/plain":_ -> MTTextPlain
133+
"text/xml":_ -> MTTextXML
134+
"application/openapi+json":_ -> MTOpenAPI
135+
"application/x-www-form-urlencoded":_ -> MTUrlEncoded
136+
"application/octet-stream":_ -> MTOctetStream
137+
"application/vnd.pgrst.plan":rest -> getPlan PlanText rest
138+
"application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest
139+
"application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest
140+
"application/vnd.pgrst.object+json":rest -> checkSingularNullStrip rest
141+
"application/vnd.pgrst.object":rest -> checkSingularNullStrip rest
142+
"application/vnd.pgrst.array+json":rest -> checkArrayNullStrip rest
143+
"*/*":_ -> MTAny
144+
other:_ -> MTOther other
145+
_ -> MTAny
128146
where
147+
checkArrayNullStrip ["nulls=stripped"] = MTArrayJSONStrip
148+
checkArrayNullStrip _ = MTApplicationJSON
149+
150+
checkSingularNullStrip ["nulls=stripped"] = MTSingularJSON True
151+
checkSingularNullStrip _ = MTSingularJSON False
152+
129153
getPlan fmt rest =
130154
let
131155
opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest)

src/PostgREST/Plan.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,8 @@ mediaToAggregate mt binField apiReq@ApiRequest{iAction=act, iPreferences=Prefere
847847
if noAgg then NoAgg
848848
else case mt of
849849
MTApplicationJSON -> BuiltinAggJson
850-
MTSingularJSON -> BuiltinAggSingleJson
850+
MTSingularJSON strip -> BuiltinAggSingleJson strip
851+
MTArrayJSONStrip -> BuiltinAggArrayJsonStrip
851852
MTGeoJSON -> BuiltinAggGeoJson
852853
MTTextCSV -> BuiltinAggCsv
853854
MTAny -> BuiltinAggJson

src/PostgREST/Query.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan, mrResAgg} apiReq@ApiRequest{
206206
failNotSingular :: MediaType -> ResultSet -> DbHandler ()
207207
failNotSingular _ RSPlan{} = pure ()
208208
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
209-
when (mediaType == MTSingularJSON && queryTotal /= 1) $ do
209+
when (elem mediaType [MTSingularJSON True,MTSingularJSON False] && queryTotal /= 1) $ do
210210
lift SQL.condemn
211211
throwError $ Error.singularityError queryTotal
212212

src/PostgREST/Query/SqlFragment.hs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -186,24 +186,29 @@ asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
186186
")"
187187
asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
188188

189-
asJsonSingleF :: Maybe Routine -> SQL.Snippet
190-
asJsonSingleF rout
191-
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')"
192-
| otherwise = "coalesce(json_agg(_postgrest_t)->0, 'null')"
189+
addNullsToSnip :: Bool -> SQL.Snippet -> SQL.Snippet
190+
addNullsToSnip strip snip =
191+
if strip then "json_strip_nulls(" <> snip <> ")" else snip
192+
193+
asJsonSingleF :: Maybe Routine -> Bool -> SQL.Snippet
194+
asJsonSingleF rout strip
195+
| returnsScalar = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t.pgrst_scalar)->0" <> ", 'null')"
196+
| otherwise = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t)->0" <> ", 'null')"
193197
where
194198
returnsScalar = maybe False funcReturnsScalar rout
195199

196-
asJsonF :: Maybe Routine -> SQL.Snippet
197-
asJsonF rout
198-
| returnsSingleComposite = "coalesce(json_agg(_postgrest_t)->0, 'null')"
199-
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')"
200-
| returnsSetOfScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar), '[]')"
201-
| otherwise = "coalesce(json_agg(_postgrest_t), '[]')"
200+
asJsonF :: Maybe Routine -> Bool -> SQL.Snippet
201+
asJsonF rout strip
202+
| returnsSingleComposite = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t)->0" <> ", 'null')"
203+
| returnsScalar = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t.pgrst_scalar)->0" <> ", 'null')"
204+
| returnsSetOfScalar = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t.pgrst_scalar)" <> ", '[]')"
205+
| otherwise = "coalesce(" <> addNullsToSnip strip "json_agg(_postgrest_t)" <> ", '[]')"
202206
where
203207
(returnsSingleComposite, returnsScalar, returnsSetOfScalar) = case rout of
204208
Just r -> (funcReturnsSingleComposite r, funcReturnsScalar r, funcReturnsSetOfScalar r)
205209
Nothing -> (False, False, False)
206210

211+
207212
asXmlF :: Maybe FieldName -> SQL.Snippet
208213
asXmlF (Just fieldName) = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
209214
-- 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
493498

494499
aggF :: Maybe Routine -> ResultAggregate -> SQL.Snippet
495500
aggF rout = \case
496-
BuiltinAggJson -> asJsonF rout
497-
BuiltinAggSingleJson -> asJsonSingleF rout
498-
BuiltinAggGeoJson -> asGeoJsonF
499-
BuiltinAggCsv -> asCsvF
500-
BuiltinAggXml bField -> asXmlF bField
501-
BuiltinAggBinary bField -> asBinaryF bField
502-
NoAgg -> "''::text"
501+
BuiltinAggJson -> asJsonF rout False
502+
BuiltinAggArrayJsonStrip -> asJsonF rout True
503+
BuiltinAggSingleJson strip -> asJsonSingleF rout strip
504+
BuiltinAggGeoJson -> asGeoJsonF
505+
BuiltinAggCsv -> asCsvF
506+
BuiltinAggXml bField -> asXmlF bField
507+
BuiltinAggBinary bField -> asBinaryF bField
508+
NoAgg -> "''::text"

src/PostgREST/Response/OpenAPI.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
350350
& summary .~ pSum
351351
& description .~ mfilter (/="") pDesc
352352
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
353-
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON]
353+
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False]
354354
& at 200 ?~ "OK"
355355
getOp = procOp
356356
& parameters .~ makeProcGetParams (pdParams pd)
@@ -406,8 +406,8 @@ postgrestSpec (prettyVersion, docsVersion) rels pds ti (s, h, p, b) sd allowSecu
406406
& definitions .~ fromList (makeTableDef rels <$> ti)
407407
& parameters .~ fromList (makeParamDefs ti)
408408
& paths .~ makePathItems pds ti
409-
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
410-
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
409+
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
410+
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON True, MTSingularJSON False, MTTextCSV]
411411
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
412412
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
413413
where

src/PostgREST/SchemaCache/Routine.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ type RoutineMap = HM.HashMap QualifiedIdentifier [Routine]
9090

9191
data ResultAggregate
9292
= BuiltinAggJson
93-
| BuiltinAggSingleJson
93+
| BuiltinAggSingleJson Bool
94+
| BuiltinAggArrayJsonStrip
9495
| BuiltinAggGeoJson
9596
| BuiltinAggCsv
9697
| BuiltinAggXml (Maybe FieldName)

test/spec/Feature/Query/NullsStrip.hs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module Feature.Query.NullsStrip where
2+
3+
import Network.Wai (Application)
4+
5+
import Network.HTTP.Types
6+
import Test.Hspec
7+
import Test.Hspec.Wai
8+
import Test.Hspec.Wai.JSON
9+
10+
import Protolude hiding (get)
11+
import SpecHelper
12+
13+
spec :: SpecWith ((), Application)
14+
spec =
15+
describe "Stripping null values from JSON response" $ do
16+
let arrayStrip = ("Accept", "application/vnd.pgrst.array+json;nulls=stripped")
17+
let singularStrip = ("Accept", "application/vnd.pgrst.object+json;nulls=stripped")
18+
19+
context "strip nulls from response" $ do
20+
it "strips nulls when Accept: application/vnd.pgrst.array+json;nulls=stripped" $
21+
request methodGet "/organizations?select=*"
22+
[arrayStrip]
23+
""
24+
`shouldRespondWith`
25+
[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}]|]
26+
{ matchStatus = 200
27+
, matchHeaders = [matchCTArrayStrip]
28+
}
29+
30+
it "strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped" $
31+
request methodGet "/organizations?limit=1"
32+
[singularStrip]
33+
""
34+
`shouldRespondWith`
35+
[json|{"id":1,"name":"Referee Org","manager_id":1}|]
36+
{ matchStatus = 200
37+
, matchHeaders = [matchCTSingularStrip]
38+
}
39+
40+
it "throws error when Accept: application/vnd.pgrst.object+json;nulls=stripped and result not singular" $
41+
request methodGet "/organizations?select=*"
42+
[singularStrip]
43+
""
44+
`shouldRespondWith`
45+
[json|{"details":"The result contains 6 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
46+
{ matchStatus = 406
47+
, matchHeaders = [matchContentTypeSingular]
48+
}
49+
50+
context "strip nulls from response even if explicitly selected" $ do
51+
it "strips nulls when Accept: application/vnd.pgrst.array+json;nulls=stripped" $
52+
request methodGet "/organizations?select=id,referee,auditor"
53+
[arrayStrip]
54+
""
55+
`shouldRespondWith`
56+
[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}]|]
57+
{ matchStatus = 200
58+
, matchHeaders = [matchCTArrayStrip]
59+
}
60+
61+
it "strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped" $
62+
request methodGet "/organizations?select=id,referee,auditor&limit=1"
63+
[singularStrip]
64+
""
65+
`shouldRespondWith`
66+
[json|{"id":1}|]
67+
{ matchStatus = 200
68+
, matchHeaders = [matchCTSingularStrip]
69+
}
70+

test/spec/Main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import qualified Feature.Query.HtmlRawOutputSpec
4545
import qualified Feature.Query.InsertSpec
4646
import qualified Feature.Query.JsonOperatorSpec
4747
import qualified Feature.Query.MultipleSchemaSpec
48+
import qualified Feature.Query.NullsStrip
4849
import qualified Feature.Query.PgSafeUpdateSpec
4950
import qualified Feature.Query.PlanSpec
5051
import qualified Feature.Query.PostGISSpec
@@ -139,6 +140,7 @@ main = do
139140
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
140141
, ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion)
141142
, ("Feature.Query.SingularSpec" , Feature.Query.SingularSpec.spec)
143+
, ("Feature.Query.NullsStrip" , Feature.Query.NullsStrip.spec)
142144
, ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec actualPgVersion)
143145
, ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion)
144146
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)

test/spec/SpecHelper.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"
4040
matchContentTypeSingular :: MatchHeader
4141
matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"
4242

43+
matchCTArrayStrip :: MatchHeader
44+
matchCTArrayStrip = "Content-Type" <:> "application/vnd.pgrst.array+json;nulls=stripped; charset=utf-8"
45+
46+
matchCTSingularStrip :: MatchHeader
47+
matchCTSingularStrip = "Content-Type" <:> "application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8"
48+
4349
matchHeaderAbsent :: HeaderName -> MatchHeader
4450
matchHeaderAbsent name = MatchHeader $ \headers _body ->
4551
case lookup name headers of

0 commit comments

Comments
 (0)