From 15568e4f0303101c5d3c77afe5b761be5cb084f9 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 27 Mar 2024 20:30:13 -0500 Subject: [PATCH] Allow alias and hint for columns in query string --- src/PostgREST/ApiRequest.hs | 12 ++++++------ src/PostgREST/ApiRequest/QueryParams.hs | 26 +++++++++++++------------ src/PostgREST/ApiRequest/Types.hs | 10 ++++++++++ src/PostgREST/Plan.hs | 10 ++++++++-- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index 6b27ca34e54..9b8d4cdb89e 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -46,6 +46,7 @@ import Web.Cookie (parseCookies) import PostgREST.ApiRequest.QueryParams (QueryParams (..)) import PostgREST.ApiRequest.Types (ApiRequestError (..), + ColumnItem (..), RangeError (..)) import PostgREST.Config (AppConfig (..), OpenAPIMode (..)) @@ -55,8 +56,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange, hasLimitZero, rangeRequested) import PostgREST.SchemaCache (SchemaCache (..)) -import PostgREST.SchemaCache.Identifiers (FieldName, - QualifiedIdentifier (..), +import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), Schema) import qualified PostgREST.ApiRequest.Preferences as Preferences @@ -117,7 +117,7 @@ data ApiRequest = ApiRequest { , iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions , iPreferences :: Preferences.Preferences -- ^ Prefer header values , iQueryParams :: QueryParams.QueryParams - , iColumns :: S.Set (Tree FieldName) -- ^ parsed colums from &columns parameter and payload + , iColumns :: S.Set (Tree ColumnItem) -- ^ parsed colums from &columns parameter and payload , iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers , iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies , iPath :: ByteString -- ^ Raw request path @@ -237,12 +237,12 @@ getRanges method QueryParams{qsOrder,qsRanges} hdrs isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange) topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows -getPayload :: RequestBody -> MediaType -> QueryParams.QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set (Tree FieldName)) +getPayload :: RequestBody -> MediaType -> QueryParams.QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set (Tree ColumnItem)) getPayload reqBody contentMediaType QueryParams{qsColumns} action = do checkedPayload <- if shouldParsePayload then payload else Right Nothing let cols = case (checkedPayload, columns) of - (Just ProcessedJSON{payKeys}, _) -> S.map (`Node` []) payKeys - (Just ProcessedUrlEncoded{payKeys}, _) -> S.map (`Node` []) payKeys + (Just ProcessedJSON{payKeys}, _) -> S.map ((`Node` []) . ColumnField) payKeys + (Just ProcessedUrlEncoded{payKeys}, _) -> S.map ((`Node` []) . ColumnField) payKeys (Just RawJSON{}, Just cls) -> S.fromList cls _ -> S.empty return (checkedPayload, cols) diff --git a/src/PostgREST/ApiRequest/QueryParams.hs b/src/PostgREST/ApiRequest/QueryParams.hs index 8d4e431e954..1ede04ed322 100644 --- a/src/PostgREST/ApiRequest/QueryParams.hs +++ b/src/PostgREST/ApiRequest/QueryParams.hs @@ -44,10 +44,10 @@ import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.SchemaCache.Identifiers (FieldName) import PostgREST.ApiRequest.Types (AggregateFunction (..), - EmbedParam (..), EmbedPath, Field, - Filter (..), FtsOperator (..), - Hint, JoinType (..), - JsonOperand (..), + ColumnItem (..), EmbedParam (..), + EmbedPath, Field, Filter (..), + FtsOperator (..), Hint, + JoinType (..), JsonOperand (..), JsonOperation (..), JsonPath, ListVal, LogicOperator (..), LogicTree (..), OpExpr (..), @@ -73,7 +73,7 @@ data QueryParams = -- ^ &order parameters for each level , qsLogic :: [(EmbedPath, LogicTree)] -- ^ &and and &or parameters used for complex boolean logic - , qsColumns :: Maybe [Tree FieldName] + , qsColumns :: Maybe [Tree ColumnItem] -- ^ &columns parameter and payload , qsSelect :: [Tree SelectItem] -- ^ &select parameter used to shape the response @@ -261,8 +261,8 @@ pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree P.parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") $ toS (op <> v) --- Satisfies the form: /products?columns=name,suppliers(name) -pRequestColumns :: Maybe Text -> Either QPError (Maybe [Tree FieldName]) +-- Satisfies the form: /products?columns=name,sup:suppliers!fk(name) +pRequestColumns :: Maybe Text -> Either QPError (Maybe [Tree ColumnItem]) pRequestColumns colStr = case colStr of Just str -> @@ -479,11 +479,13 @@ pRelationSelect = lexeme $ do try (void $ lookAhead (string "(")) return $ SelectRelation name alias hint jType -pRelationColumn :: Parser FieldName +pRelationColumn :: Parser ColumnItem pRelationColumn = lexeme $ do + alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) name <- pFieldName + (hint, _) <- pEmbedParams try (void $ lookAhead (string "(")) - return name + return $ ColumnRelation name alias hint -- | -- Parse regular fields in select @@ -852,17 +854,17 @@ pLogicPath = do pColumns :: Parser [FieldName] pColumns = pFieldName `sepBy1` lexeme (char ',') -pColumnForest :: Parser [Tree FieldName] +pColumnForest :: Parser [Tree ColumnItem] pColumnForest = pColumnTree `sepBy1` lexeme (char ',') where pColumnTree = Node <$> try pRelationColumn <*> between (char '(') (char ')') pColumnForest <|> Node <$> pColumnName <*> pure [] -pColumnName :: Parser FieldName +pColumnName :: Parser ColumnItem pColumnName = lexeme $ do fld <- pFieldName pEnd - return fld + return $ ColumnField fld where pEnd = try (void $ lookAhead (string ")")) <|> try (void $ lookAhead (string ",")) <|> diff --git a/src/PostgREST/ApiRequest/Types.hs b/src/PostgREST/ApiRequest/Types.hs index b4c07595455..a993f8c61c8 100644 --- a/src/PostgREST/ApiRequest/Types.hs +++ b/src/PostgREST/ApiRequest/Types.hs @@ -32,6 +32,7 @@ module PostgREST.ApiRequest.Types , QuantOperator(..) , FtsOperator(..) , SelectItem(..) + , ColumnItem(..) ) where import PostgREST.MediaType (MediaType (..)) @@ -67,6 +68,15 @@ data SelectItem } deriving (Eq, Show) +data ColumnItem + = ColumnField FieldName + | ColumnRelation + { colRelation :: FieldName + , colAlias :: Maybe Alias + , colHint :: Maybe Hint + } + deriving (Eq, Show, Ord) + data ApiRequestError = AggregatesNotAllowed | AmbiguousRelBetween Text Text [Relationship] diff --git a/src/PostgREST/Plan.hs b/src/PostgREST/Plan.hs index 06d55306619..28e1d073cb8 100644 --- a/src/PostgREST/Plan.hs +++ b/src/PostgREST/Plan.hs @@ -151,7 +151,7 @@ callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do let paramKeys = case invMethod of InvRead _ -> S.fromList $ fst <$> qsParams' - Inv -> S.map rootLabel iColumns + Inv -> S.map (getColumnAsText . rootLabel) iColumns proc@Function{..} <- mapLeft ApiRequestError $ findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) iContentMediaType (invMethod == Inv) let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations @@ -923,7 +923,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{ combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates) applyDefaults = preferMissing == Just ApplyDefaults - typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList (S.map rootLabel iColumns) + typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList (S.map (getColumnAsText . rootLabel) iColumns) resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField resolveOrError _ Nothing _ = Left NotFound @@ -1032,3 +1032,9 @@ negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRep when' :: Bool -> Maybe a -> Maybe a when' True (Just a) = Just a when' _ _ = Nothing + +getColumnAsText :: ColumnItem -> Text +getColumnAsText colItem = + case colItem of + ColumnField col -> col + ColumnRelation{colRelation} -> colRelation