Skip to content

Commit 9ec5b03

Browse files
taimoorzaeemsteve-chavez
authored andcommitted
refactor: move content negotiation logic to Negotiate.hs
Moves the `negotiateContent` function to `Plan/Negotiate.hs` module. This also adds comments to describe the current approach we have for negotiation. Signed-off-by: Taimoor Zaeem <[email protected]>
1 parent 76e0e1f commit 9ec5b03

File tree

3 files changed

+84
-35
lines changed

3 files changed

+84
-35
lines changed

postgrest.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ library
8181
PostgREST.Plan
8282
PostgREST.Plan.CallPlan
8383
PostgREST.Plan.MutatePlan
84+
PostgREST.Plan.Negotiate
8485
PostgREST.Plan.ReadPlan
8586
PostgREST.Plan.Types
8687
PostgREST.RangeQuery

src/PostgREST/Plan.hs

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ import PostgREST.Error (ApiRequestError (..),
4343
Error (..),
4444
SchemaCacheError (..))
4545
import PostgREST.MediaType (MediaType (..))
46+
import PostgREST.Plan.Negotiate (negotiateContent)
4647
import PostgREST.Query.SqlFragment (sourceCTEName)
4748
import PostgREST.RangeQuery (NonnegRange, allRange,
4849
convertToLimitZeroRange,
4950
restrictRange)
5051
import PostgREST.SchemaCache (SchemaCache (..))
5152
import PostgREST.SchemaCache.Identifiers (FieldName,
5253
QualifiedIdentifier (..),
53-
RelIdentifier (..),
5454
Schema)
5555
import PostgREST.SchemaCache.Relationship (Cardinality (..),
5656
Junction (..),
@@ -60,8 +60,6 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
6060
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
6161
RepresentationsMap)
6262
import PostgREST.SchemaCache.Routine (MediaHandler (..),
63-
MediaHandlerMap,
64-
ResolvedHandler,
6563
Routine (..),
6664
RoutineMap,
6765
RoutineParam (..),
@@ -1116,35 +1114,3 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
11161114
-- they are later concatenated with AND in the QueryBuilder
11171115
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
11181116
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
1119-
1120-
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
1121-
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
1122-
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
1123-
case (act, firstAcceptedPick) of
1124-
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
1125-
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
1126-
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
1127-
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
1128-
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
1129-
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
1130-
(_, Just (x, mt)) -> Right (x, mt)
1131-
where
1132-
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
1133-
matchMT mt = case mt of
1134-
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
1135-
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
1136-
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
1137-
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
1138-
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
1139-
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
1140-
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
1141-
-- all the other media types can be overridden
1142-
x -> lookupHandler x
1143-
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
1144-
lookupHandler mt =
1145-
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
1146-
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
1147-
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
1148-
when' :: Bool -> Maybe a -> Maybe a
1149-
when' True (Just a) = Just a
1150-
when' _ _ = Nothing

src/PostgREST/Plan/Negotiate.hs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{-|
2+
Module : PostgREST.Plan.Negotiate
3+
Description : PostgREST Content Negotiation
4+
5+
This module contains logic for content negotiation.
6+
RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-3.4
7+
-}
8+
9+
module PostgREST.Plan.Negotiate
10+
( negotiateContent
11+
) where
12+
13+
import qualified Data.HashMap.Strict as HM
14+
15+
import PostgREST.ApiRequest (ApiRequest (..))
16+
import PostgREST.Config (AppConfig (..))
17+
import PostgREST.Error (ApiRequestError (..))
18+
import PostgREST.MediaType (MediaType (..))
19+
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
20+
RelIdentifier (..))
21+
import PostgREST.SchemaCache.Routine (MediaHandler (..),
22+
MediaHandlerMap,
23+
ResolvedHandler)
24+
25+
import PostgREST.ApiRequest.Preferences
26+
import PostgREST.ApiRequest.Types
27+
import qualified PostgREST.MediaType as MediaType
28+
29+
import Protolude hiding (from)
30+
31+
-- We have two general cases of return values from database objects
32+
-- (tables/views/functions):
33+
--
34+
-- 1. "un-mime-typed" values, in most of the cases this is a composite/row
35+
-- value, for example for tables or views, but also often for functions.
36+
-- It can be simple integer values or text or bytea as well.
37+
--
38+
-- For this, we need handlers to transform the "non-mime-typed" values
39+
-- into "mimetypes". We have a default builtin handler that does
40+
-- "application/json". We can add more handlers via aggregates.
41+
--
42+
-- 2. "mime-typed" values, which specifically return a domain type that is
43+
-- associated to a certain mimetype. e.g, a function returning only
44+
-- "image/png".
45+
--
46+
-- FIXME:
47+
-- If the function returns a domain type - let's say image/png, we should
48+
-- accept */*, image/*, and image/png.
49+
-- Related issue: https://github.com/PostgREST/postgrest/issues/3391
50+
51+
-- | Do content negotiation. i.e. choose a media type based on the
52+
-- intersection of accepted/produced media types.
53+
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
54+
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
55+
case (act, firstAcceptedPick) of
56+
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
57+
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
58+
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
59+
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
60+
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
61+
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
62+
(_, Just (x, mt)) -> Right (x, mt)
63+
where
64+
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
65+
matchMT mt = case mt of
66+
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
67+
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
68+
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
69+
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
70+
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
71+
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
72+
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
73+
-- all the other media types can be overridden
74+
x -> lookupHandler x
75+
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
76+
lookupHandler mt =
77+
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
78+
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
79+
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
80+
when' :: Bool -> Maybe a -> Maybe a
81+
when' True (Just a) = Just a
82+
when' _ _ = Nothing

0 commit comments

Comments
 (0)