Skip to content

Commit f8e8e36

Browse files
committed
refactor: stricter plan media type
1 parent c1a8661 commit f8e8e36

File tree

6 files changed

+53
-51
lines changed

6 files changed

+53
-51
lines changed

src/PostgREST/ApiRequest.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ import PostgREST.ApiRequest.Types (ApiRequestError (..),
5151
RangeError (..))
5252
import PostgREST.Config (AppConfig (..),
5353
OpenAPIMode (..))
54-
import PostgREST.MediaType (MediaType (..))
54+
import PostgREST.MediaType (MTPlanFormat (..),
55+
MediaType (..))
5556
import PostgREST.RangeQuery (NonnegRange, allRange,
5657
convertToLimitZeroRange,
5758
hasLimitZero,
@@ -363,15 +364,15 @@ producedMediaTypes conf action path =
363364
case action of
364365
ActionRead _ -> defaultMediaTypes ++ rawMediaTypes
365366
ActionInvoke _ -> invokeMediaTypes
366-
ActionInspect _ -> [MTOpenAPI, MTApplicationJSON, MTAny]
367367
ActionInfo -> defaultMediaTypes
368368
ActionMutate _ -> defaultMediaTypes
369+
ActionInspect _ -> [MTOpenAPI, MTApplicationJSON, MTAny]
369370
where
370371
invokeMediaTypes =
371372
defaultMediaTypes
372373
++ rawMediaTypes
373374
++ [MTOpenAPI | pathIsRootSpec path]
374375
defaultMediaTypes =
375376
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV] ++
376-
[MTPlan Nothing Nothing mempty | configDbPlanEnabled conf] ++ [MTAny]
377+
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
377378
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]

src/PostgREST/MediaType.hs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ module PostgREST.MediaType
1212

1313
import qualified Data.ByteString as BS
1414
import qualified Data.ByteString.Internal as BS (c2w)
15-
import Data.Maybe (fromJust)
1615

1716
import Network.HTTP.Types.Header (Header, hContentType)
1817

@@ -39,7 +38,7 @@ data MediaType
3938
| MTOctetStream
4039
| MTAny
4140
| MTOther ByteString
42-
| MTPlan (Maybe MediaType) (Maybe MTPlanFormat) [MTPlanOption]
41+
| MTPlan MediaType MTPlanFormat [MTPlanOption]
4342
instance Eq MediaType where
4443
MTApplicationJSON == MTApplicationJSON = True
4544
MTSingularJSON == MTSingularJSON = True
@@ -84,8 +83,8 @@ toMime MTOctetStream = "application/octet-stream"
8483
toMime MTAny = "*/*"
8584
toMime (MTOther ct) = ct
8685
toMime (MTPlan mt fmt opts) =
87-
"application/vnd.pgrst.plan" <> maybe mempty (\x -> "+" <> toMimePlanFormat x) fmt <>
88-
(if isNothing mt then mempty else "; for=\"" <> toMime (fromJust mt) <> "\"") <>
86+
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
87+
("; for=\"" <> toMime mt <> "\"") <>
8988
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
9089

9190
toMimePlanOption :: MTPlanOption -> ByteString
@@ -105,13 +104,13 @@ toMimePlanFormat PlanText = "text"
105104
-- MTApplicationJSON
106105
--
107106
-- >>> decodeMediaType "application/vnd.pgrst.plan;"
108-
-- MTPlan Nothing Nothing []
107+
-- MTPlan MTApplicationJSON PlanText []
109108
--
110109
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
111-
-- MTPlan (Just MTApplicationJSON) Nothing []
110+
-- MTPlan MTApplicationJSON PlanText []
112111
--
113-
-- >>> decodeMediaType "application/vnd.pgrst.plan+text;for=\"text/csv\""
114-
-- MTPlan (Just MTTextCSV) (Just PlanText) []
112+
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
113+
-- MTPlan MTTextCSV PlanJSON []
115114
decodeMediaType :: BS.ByteString -> MediaType
116115
decodeMediaType mt =
117116
case BS.split (BS.c2w ';') mt of
@@ -125,28 +124,31 @@ decodeMediaType mt =
125124
"application/vnd.pgrst.object":_ -> MTSingularJSON
126125
"application/x-www-form-urlencoded":_ -> MTUrlEncoded
127126
"application/octet-stream":_ -> MTOctetStream
128-
"application/vnd.pgrst.plan":rest -> getPlan Nothing rest
129-
"application/vnd.pgrst.plan+text":rest -> getPlan (Just PlanText) rest
130-
"application/vnd.pgrst.plan+json":rest -> getPlan (Just PlanJSON) rest
127+
"application/vnd.pgrst.plan":rest -> getPlan PlanText rest
128+
"application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest
129+
"application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest
131130
"*/*":_ -> MTAny
132131
other:_ -> MTOther other
133132
_ -> MTAny
134133
where
135134
getPlan fmt rest =
136-
let
137-
opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest)
138-
inOpts str = str `elem` opts
139-
mtFor = decodeMediaType . dropAround (== BS.c2w '"') <$> (BS.stripPrefix "for=" =<< find (BS.isPrefixOf "for=") rest)
140-
dropAround p = BS.dropWhile p . BS.dropWhileEnd p in
141-
MTPlan mtFor fmt $
142-
[PlanAnalyze | inOpts "analyze" ] ++
143-
[PlanVerbose | inOpts "verbose" ] ++
144-
[PlanSettings | inOpts "settings"] ++
145-
[PlanBuffers | inOpts "buffers" ] ++
146-
[PlanWAL | inOpts "wal" ]
135+
let
136+
opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest)
137+
inOpts str = str `elem` opts
138+
dropAround p = BS.dropWhile p . BS.dropWhileEnd p
139+
mtFor = fromMaybe MTApplicationJSON $ do
140+
foundFor <- find (BS.isPrefixOf "for=") rest
141+
strippedFor <- BS.stripPrefix "for=" foundFor
142+
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor
143+
in
144+
MTPlan mtFor fmt $
145+
[PlanAnalyze | inOpts "analyze" ] ++
146+
[PlanVerbose | inOpts "verbose" ] ++
147+
[PlanSettings | inOpts "settings"] ++
148+
[PlanBuffers | inOpts "buffers" ] ++
149+
[PlanWAL | inOpts "wal" ]
147150

148151
getMediaType :: MediaType -> MediaType
149152
getMediaType mt = case mt of
150-
MTPlan (Just mType) _ _ -> mType
151-
MTPlan Nothing _ _ -> MTApplicationJSON
152-
other -> other
153+
MTPlan mType _ _ -> mType
154+
other -> other

src/PostgREST/Plan.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,10 +633,10 @@ binaryField AppConfig{configRawMediaTypes} acceptMediaType proc rpTree
633633
where
634634
isRawMediaType = acceptMediaType `elem` configRawMediaTypes `L.union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan acceptMediaType
635635
isRawPlan mt = case mt of
636-
MTPlan (Just MTOctetStream) _ _ -> True
637-
MTPlan (Just MTTextPlain) _ _ -> True
638-
MTPlan (Just MTTextXML) _ _ -> True
639-
_ -> False
636+
MTPlan MTOctetStream _ _ -> True
637+
MTPlan MTTextPlain _ _ -> True
638+
MTPlan MTTextXML _ _ -> True
639+
_ -> False
640640

641641
fstFieldName :: ReadPlanTree -> Maybe FieldName
642642
fstFieldName (Node ReadPlan{select=(("*", []), _, _):_} []) = Nothing

src/PostgREST/Query/SqlFragment.hs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ intercalateSnippet :: ByteString -> [SQL.Snippet] -> SQL.Snippet
431431
intercalateSnippet _ [] = mempty
432432
intercalateSnippet frag snippets = foldr1 (\a b -> a <> SQL.sql frag <> b) snippets
433433

434-
explainF :: Maybe MTPlanFormat -> [MTPlanOption] -> SQL.Snippet -> SQL.Snippet
434+
explainF :: MTPlanFormat -> [MTPlanOption] -> SQL.Snippet -> SQL.Snippet
435435
explainF fmt opts snip =
436436
"EXPLAIN (" <>
437437
SQL.sql (BS.intercalate ", " (fmtPlanFmt fmt : (fmtPlanOpt <$> opts))) <>
@@ -444,9 +444,8 @@ explainF fmt opts snip =
444444
fmtPlanOpt PlanBuffers = "BUFFERS"
445445
fmtPlanOpt PlanWAL = "WAL"
446446

447-
fmtPlanFmt Nothing = "FORMAT TEXT"
448-
fmtPlanFmt (Just PlanJSON) = "FORMAT JSON"
449-
fmtPlanFmt (Just PlanText) = "FORMAT TEXT"
447+
fmtPlanFmt PlanText = "FORMAT TEXT"
448+
fmtPlanFmt PlanJSON = "FORMAT JSON"
450449

451450
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
452451
setConfigLocal :: ByteString -> (ByteString, ByteString) -> SQL.Snippet

src/PostgREST/Query/Statements.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ preparePlanRows :: SQL.Snippet -> Bool -> SQL.Statement () (Maybe Int64)
167167
preparePlanRows countQuery =
168168
SQL.dynamicallyParameterized snippet decodeIt
169169
where
170-
snippet = explainF (Just PlanJSON) mempty countQuery
170+
snippet = explainF PlanJSON mempty countQuery
171171
decodeIt :: HD.Result (Maybe Int64)
172172
decodeIt =
173173
let row = HD.singleRow $ column HD.bytea in

test/spec/Feature/Query/PlanSpec.hs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ spec actualPgVersion = do
3333
resStatus = simpleStatus r
3434

3535
liftIO $ do
36-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
36+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
3737
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
3838
totalCost `shouldBe`
3939
if actualPgVersion > pgVersion120
@@ -49,7 +49,7 @@ spec actualPgVersion = do
4949
resStatus = simpleStatus r
5050

5151
liftIO $ do
52-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
52+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
5353
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
5454
totalCost `shouldBe`
5555
if actualPgVersion > pgVersion120
@@ -65,7 +65,7 @@ spec actualPgVersion = do
6565
resHeaders = simpleHeaders r
6666

6767
liftIO $ do
68-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=buffers; charset=utf-8")
68+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=buffers; charset=utf-8")
6969
resBody `shouldSatisfy` (\t -> T.isInfixOf "Shared Hit Blocks" (decodeUtf8 $ BS.toStrict t))
7070
else do
7171
-- analyze is required for buffers on pg < 13
@@ -75,7 +75,7 @@ spec actualPgVersion = do
7575
resHeaders = simpleHeaders r
7676

7777
liftIO $ do
78-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=analyze|buffers; charset=utf-8")
78+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=analyze|buffers; charset=utf-8")
7979
blocks `shouldBe` Just [aesonQQ| 1.0 |]
8080

8181
when (actualPgVersion >= pgVersion120) $
@@ -86,7 +86,7 @@ spec actualPgVersion = do
8686
resHeaders = simpleHeaders r
8787

8888
liftIO $ do
89-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=settings; charset=utf-8")
89+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=settings; charset=utf-8")
9090
searchPath `shouldBe`
9191
Just [aesonQQ|
9292
{
@@ -102,7 +102,7 @@ spec actualPgVersion = do
102102
resHeaders = simpleHeaders r
103103

104104
liftIO $ do
105-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=analyze|wal; charset=utf-8")
105+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=analyze|wal; charset=utf-8")
106106
walRecords `shouldBe` Just [aesonQQ|0|]
107107

108108
it "outputs columns info when using the verbose option" $ do
@@ -112,7 +112,7 @@ spec actualPgVersion = do
112112
resHeaders = simpleHeaders r
113113

114114
liftIO $ do
115-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=verbose; charset=utf-8")
115+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=verbose; charset=utf-8")
116116
cols `shouldBe` Just [aesonQQ| ["projects.id", "projects.name", "projects.client_id"] |]
117117

118118
it "outputs the plan for application/json " $ do
@@ -151,7 +151,7 @@ spec actualPgVersion = do
151151
resStatus = simpleStatus r
152152

153153
liftIO $ do
154-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
154+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
155155
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
156156
totalCost `shouldBe` 3.27
157157

@@ -164,7 +164,7 @@ spec actualPgVersion = do
164164
resStatus = simpleStatus r
165165

166166
liftIO $ do
167-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
167+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
168168
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
169169
totalCost `shouldBe` 12.45
170170

@@ -177,7 +177,7 @@ spec actualPgVersion = do
177177
resStatus = simpleStatus r
178178

179179
liftIO $ do
180-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
180+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
181181
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
182182
totalCost `shouldBe` 15.68
183183

@@ -191,7 +191,7 @@ spec actualPgVersion = do
191191
resStatus = simpleStatus r
192192

193193
liftIO $ do
194-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
194+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
195195
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
196196
totalCost `shouldBe` 1.29
197197

@@ -216,7 +216,7 @@ spec actualPgVersion = do
216216
resStatus = simpleStatus r
217217

218218
liftIO $ do
219-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
219+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
220220
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
221221
totalCost `shouldBe` 68.56
222222

@@ -241,7 +241,7 @@ spec actualPgVersion = do
241241
resStatus = simpleStatus r
242242

243243
liftIO $ do
244-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; charset=utf-8")
244+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
245245
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
246246
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
247247

@@ -254,7 +254,7 @@ spec actualPgVersion = do
254254
resStatus = simpleStatus r
255255

256256
liftIO $ do
257-
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan; charset=utf-8")
257+
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
258258
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
259259
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
260260

0 commit comments

Comments
 (0)