refactor: separate SchemaCacheError from ApiRequestError (#4010)
This commit is contained in:
+65
-40
@@ -10,6 +10,7 @@ module PostgREST.Error
|
||||
, ApiRequestError(..)
|
||||
, QPError(..)
|
||||
, RangeError(..)
|
||||
, SchemaCacheError(..)
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, JwtError (..)
|
||||
@@ -69,16 +70,12 @@ class ErrorBody a where
|
||||
|
||||
data ApiRequestError
|
||||
= AggregatesNotAllowed
|
||||
| AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| NotEmbedded Text
|
||||
| NotImplemented Text
|
||||
| PutLimitNotAllowedError
|
||||
@@ -87,8 +84,6 @@ data ApiRequestError
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| ColumnNotFound Text Text
|
||||
| TableNotFound Text Text [Table]
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| PutMatchingPkError
|
||||
@@ -116,8 +111,6 @@ data RangeError
|
||||
|
||||
instance PgrstError ApiRequestError where
|
||||
status AggregatesNotAllowed{} = HTTP.status400
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status MediaTypeError{} = HTTP.status406
|
||||
status InvalidBody{} = HTTP.status400
|
||||
status InvalidFilters = HTTP.status405
|
||||
@@ -125,8 +118,6 @@ instance PgrstError ApiRequestError where
|
||||
status InvalidRpcMethod{} = HTTP.status405
|
||||
status InvalidRange{} = HTTP.status416
|
||||
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status NotEmbedded{} = HTTP.status400
|
||||
status NotImplemented{} = HTTP.status400
|
||||
status PutLimitNotAllowedError = HTTP.status400
|
||||
@@ -135,8 +126,6 @@ instance PgrstError ApiRequestError where
|
||||
status UnacceptableFilter{} = HTTP.status400
|
||||
status UnacceptableSchema{} = HTTP.status406
|
||||
status UnsupportedMethod{} = HTTP.status405
|
||||
status ColumnNotFound{} = HTTP.status400
|
||||
status TableNotFound{} = HTTP.status404
|
||||
status GucHeadersError = HTTP.status500
|
||||
status GucStatusError = HTTP.status500
|
||||
status PutMatchingPkError = HTTP.status400
|
||||
@@ -192,13 +181,6 @@ instance ErrorBody ApiRequestError where
|
||||
code OpenAPIDisabled = "PGRST126"
|
||||
code NotImplemented{} = "PGRST127"
|
||||
|
||||
code NoRelBetween{} = "PGRST200"
|
||||
code AmbiguousRelBetween{} = "PGRST201"
|
||||
code NoRpc{} = "PGRST202"
|
||||
code AmbiguousRpc{} = "PGRST203"
|
||||
code ColumnNotFound{} = "PGRST204"
|
||||
code TableNotFound{} = "PGRST205"
|
||||
|
||||
-- MESSAGE: Text
|
||||
message (QueryParamError (QPError msg _)) = msg
|
||||
message (InvalidRpcMethod method) = "Cannot use the " <> T.decodeUtf8 method <> " method on RPC"
|
||||
@@ -224,19 +206,6 @@ instance ErrorBody ApiRequestError where
|
||||
message OpenAPIDisabled = "Root endpoint metadata is disabled"
|
||||
message (NotImplemented _) = "Feature not implemented"
|
||||
|
||||
message (NoRelBetween parent child _ _ _) = "Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache"
|
||||
message (AmbiguousRelBetween parent child _) = "Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'"
|
||||
message (NoRpc schema procName argumentKeys contentType isInvPost _ _) = "Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"
|
||||
where
|
||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
||||
func = schema <> "." <> procName
|
||||
prms = T.intercalate ", " argumentKeys
|
||||
prmsMsg = "(" <> prms <> ")"
|
||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||
message (AmbiguousRpc procs) = "Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]
|
||||
message (ColumnNotFound rel col) = "Could not find the '" <> col <> "' column of '" <> rel <> "' in the schema cache"
|
||||
message (TableNotFound schemaName relName _) = "Could not find the table '" <> schemaName <> "." <> relName <> "' in the schema cache"
|
||||
|
||||
-- DETAILS: Maybe JSON.Value
|
||||
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
|
||||
details (InvalidRange rangeError) = Just $
|
||||
@@ -252,6 +221,58 @@ instance ErrorBody ApiRequestError where
|
||||
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
|
||||
details (NotImplemented details') = Just $ JSON.String details'
|
||||
|
||||
details _ = Nothing
|
||||
|
||||
-- HINT: Maybe JSON.Value
|
||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
data SchemaCacheError
|
||||
= AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| ColumnNotFound Text Text
|
||||
| TableNotFound Text Text [Table]
|
||||
deriving Show
|
||||
|
||||
instance PgrstError SchemaCacheError where
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status ColumnNotFound{} = HTTP.status400
|
||||
status TableNotFound{} = HTTP.status404
|
||||
|
||||
headers _ = mempty
|
||||
|
||||
instance ErrorBody SchemaCacheError where
|
||||
code NoRelBetween{} = "PGRST200"
|
||||
code AmbiguousRelBetween{} = "PGRST201"
|
||||
code NoRpc{} = "PGRST202"
|
||||
code AmbiguousRpc{} = "PGRST203"
|
||||
code ColumnNotFound{} = "PGRST204"
|
||||
code TableNotFound{} = "PGRST205"
|
||||
|
||||
message (NoRelBetween parent child _ _ _) = "Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache"
|
||||
message (AmbiguousRelBetween parent child _) = "Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'"
|
||||
message (NoRpc schema procName argumentKeys contentType isInvPost _ _) = "Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"
|
||||
where
|
||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
||||
func = schema <> "." <> procName
|
||||
prms = T.intercalate ", " argumentKeys
|
||||
prmsMsg = "(" <> prms <> ")"
|
||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||
message (AmbiguousRpc procs) = "Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]
|
||||
message (ColumnNotFound rel col) = "Could not find the '" <> col <> "' column of '" <> rel <> "' in the schema cache"
|
||||
message (TableNotFound schemaName relName _) = "Could not find the table '" <> schemaName <> "." <> relName <> "' in the schema cache"
|
||||
|
||||
details (NoRelBetween parent child embedHint schema _) = Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."
|
||||
details (AmbiguousRelBetween _ _ rels) = Just $ JSON.toJSONList (compressedRel <$> rels)
|
||||
details (NoRpc schema procName argumentKeys contentType isInvPost _ _) =
|
||||
@@ -271,10 +292,6 @@ instance ErrorBody ApiRequestError where
|
||||
|
||||
details _ = Nothing
|
||||
|
||||
-- HINT: Maybe JSON.Value
|
||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||
|
||||
hint (NoRelBetween parent child _ schema allRels) = JSON.String <$> noRelBetweenHint parent child schema allRels
|
||||
hint (AmbiguousRelBetween _ child rels) = Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key."
|
||||
-- The hint will be null in the case of single unnamed parameter functions
|
||||
@@ -289,8 +306,7 @@ instance ErrorBody ApiRequestError where
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
instance JSON.ToJSON SchemaCacheError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
@@ -461,6 +477,8 @@ pgrstParseErrorHint err = case err of
|
||||
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
deriving Show
|
||||
|
||||
type Authenticated = Bool
|
||||
|
||||
instance PgrstError PgError where
|
||||
@@ -608,27 +626,30 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
_ -> HTTP.status500
|
||||
|
||||
|
||||
-- TODO: separate "SchemaCacheError" from ApiRequestError similar to how we
|
||||
-- group them in docs
|
||||
data Error
|
||||
= ApiRequestError ApiRequestError
|
||||
| SchemaCacheErr SchemaCacheError
|
||||
| JwtErr JwtError
|
||||
| NoSchemaCacheError
|
||||
| PgErr PgError
|
||||
deriving Show
|
||||
|
||||
data JwtError
|
||||
= JwtDecodeError Text
|
||||
| JwtSecretMissing
|
||||
| JwtTokenRequired
|
||||
| JwtClaimsError Text
|
||||
deriving Show
|
||||
|
||||
instance PgrstError Error where
|
||||
status (ApiRequestError err) = status err
|
||||
status (SchemaCacheErr err) = status err
|
||||
status (JwtErr err) = status err
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
|
||||
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err
|
||||
headers (SchemaCacheErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers (JwtErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers (PgErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty
|
||||
@@ -639,21 +660,25 @@ instance JSON.ToJSON Error where
|
||||
|
||||
instance ErrorBody Error where
|
||||
code (ApiRequestError err) = code err
|
||||
code (SchemaCacheErr err) = code err
|
||||
code (JwtErr err) = code err
|
||||
code NoSchemaCacheError = "PGRST002"
|
||||
code (PgErr err) = code err
|
||||
|
||||
message (ApiRequestError err) = message err
|
||||
message (SchemaCacheErr err) = message err
|
||||
message (JwtErr err) = message err
|
||||
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
|
||||
message (PgErr err) = message err
|
||||
|
||||
details (ApiRequestError err) = details err
|
||||
details (SchemaCacheErr err) = details err
|
||||
details (JwtErr err) = details err
|
||||
details NoSchemaCacheError = Nothing
|
||||
details (PgErr err) = details err
|
||||
|
||||
hint (ApiRequestError err) = hint err
|
||||
hint (SchemaCacheErr err) = hint err
|
||||
hint (JwtErr err) = hint err
|
||||
hint NoSchemaCacheError = Nothing
|
||||
hint (PgErr err) = hint err
|
||||
|
||||
+43
-43
@@ -45,7 +45,8 @@ import PostgREST.ApiRequest (Action (..),
|
||||
Payload (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Error (ApiRequestError (..),
|
||||
Error (..))
|
||||
Error (..),
|
||||
SchemaCacheError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
@@ -155,7 +156,7 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of
|
||||
|
||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
|
||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
||||
qi <- mapLeft ApiRequestError $ findTable identifier (dbTables sCache)
|
||||
qi <- findTable identifier (dbTables sCache)
|
||||
rPlan <- readPlan qi conf sCache apiRequest
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
@@ -163,7 +164,7 @@ wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Prefe
|
||||
|
||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
||||
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
||||
qi <- mapLeft ApiRequestError $ findTable identifier (dbTables sCache)
|
||||
qi <- findTable identifier (dbTables sCache)
|
||||
rPlan <- readPlan qi conf sCache apiRequest
|
||||
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
@@ -175,7 +176,7 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
||||
let paramKeys = case invMethod of
|
||||
InvRead _ -> S.fromList $ fst <$> qsParams'
|
||||
Inv -> iColumns
|
||||
proc@Function{..} <- mapLeft ApiRequestError $
|
||||
proc@Function{..} <- mapLeft SchemaCacheErr $
|
||||
findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv)
|
||||
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
||||
rPlan <- readPlan relIdentifier conf sCache apiRequest
|
||||
@@ -212,7 +213,7 @@ inspectPlan apiRequest headersOnly schema = do
|
||||
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
||||
-}
|
||||
findProc :: QualifiedIdentifier -> S.Set Text -> RoutineMap -> MediaType -> Bool -> Either ApiRequestError Routine
|
||||
findProc :: QualifiedIdentifier -> S.Set Text -> RoutineMap -> MediaType -> Bool -> Either SchemaCacheError Routine
|
||||
findProc qi argumentsKeys allProcs contentMediaType isInvPost =
|
||||
case matchProc of
|
||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
||||
@@ -337,7 +338,6 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
|
||||
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
in
|
||||
mapLeft ApiRequestError $
|
||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||
addToManyOrderSelects =<<
|
||||
hoistSpreadAggFunctions =<<
|
||||
@@ -382,7 +382,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
-- * A select term with a JSON path
|
||||
-- * Domain representations
|
||||
-- * Aggregates in spread relationships
|
||||
addAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addAliases :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addAliases = Right . fmap addAliasToPlan
|
||||
where
|
||||
addAliasToPlan rp@ReadPlan{select=sel, relSpread=spr} = rp{select=map (aliasSelectField $ isJust spr) sel}
|
||||
@@ -433,7 +433,7 @@ knownColumnsInContext ResolverContext{..} =
|
||||
-- * When there are data representations present.
|
||||
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
||||
-- * When the ReadPlan is a to-many spread relationship
|
||||
expandStars :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
expandStars :: ResolverContext -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
||||
where
|
||||
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
|
||||
@@ -478,7 +478,7 @@ expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@R
|
||||
expandStarSelectField _ _ selectField = [selectField]
|
||||
|
||||
-- | Enforces the `max-rows` config on the result
|
||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request
|
||||
treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> request
|
||||
where
|
||||
@@ -487,7 +487,7 @@ treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> reque
|
||||
|
||||
-- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064)
|
||||
-- also adds aliasing
|
||||
addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,relSpread,depth} forest) =
|
||||
case parentNode of
|
||||
Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) ->
|
||||
@@ -523,7 +523,7 @@ addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,re
|
||||
in
|
||||
Node newReadPlan <$> updateForest (Just $ Node newReadPlan forest)
|
||||
where
|
||||
updateForest :: Maybe ReadPlanTree -> Either ApiRequestError [ReadPlanTree]
|
||||
updateForest :: Maybe ReadPlanTree -> Either Error [ReadPlanTree]
|
||||
updateForest rq = addRels schema action allRels rq `traverse` forest
|
||||
|
||||
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]
|
||||
@@ -553,12 +553,12 @@ getJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=
|
||||
-- request is ambiguous and we return an error. In that case the request can
|
||||
-- be disambiguated by adding precision to the target or by using a hint:
|
||||
-- /origin?select=target!hint(*). The origin can be a table or view.
|
||||
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
||||
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either Error Relationship
|
||||
findRel schema allRels origin target hint =
|
||||
case rels of
|
||||
[] -> Left $ NoRelBetween origin target hint schema allRels
|
||||
[] -> Left $ SchemaCacheErr $ NoRelBetween origin target hint schema allRels
|
||||
[r] -> Right r
|
||||
rs -> Left $ AmbiguousRelBetween origin target rs
|
||||
rs -> Left $ SchemaCacheErr $ AmbiguousRelBetween origin target rs
|
||||
where
|
||||
matchFKSingleCol hint_ card = case card of
|
||||
O2M{relColumns=[(col, _)]} -> hint_ == col
|
||||
@@ -634,7 +634,7 @@ findRel schema allRels origin target hint =
|
||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
|
||||
|
||||
|
||||
addRelSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRelSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRelSelects node@(Node rp forest)
|
||||
| null forest = Right node
|
||||
| otherwise =
|
||||
@@ -699,7 +699,7 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
|
||||
-- No hoisting is done for to-many spreads
|
||||
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
|
||||
|
||||
hoistSpreadAggFunctions :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
hoistSpreadAggFunctions :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
|
||||
|
||||
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
|
||||
@@ -761,9 +761,9 @@ hoistIntoRelSelectFields _ r = r
|
||||
-- * It removes the ordering done in the ReadPlan and moves it to the SpreadType.
|
||||
-- We also select the ordering columns and alias them to avoid collisions. This is because it would be impossible
|
||||
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
||||
addToManyOrderSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
||||
| anyAggSel || anyAggRelSel = Left $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestError $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
||||
where
|
||||
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
||||
@@ -783,19 +783,19 @@ addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, r
|
||||
selOrdAlias name i = relAggAlias <> "_" <> name <> "_" <> show i -- add index to avoid collisions in aliases
|
||||
addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `traverse` forest
|
||||
|
||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left AggregatesNotAllowed
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed
|
||||
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||
|
||||
-- | Lookup table in the schema cache before creating read plan
|
||||
findTable :: QualifiedIdentifier -> TablesMap -> Either ApiRequestError QualifiedIdentifier
|
||||
findTable :: QualifiedIdentifier -> TablesMap -> Either Error QualifiedIdentifier
|
||||
findTable qi@QualifiedIdentifier{..} tableMap =
|
||||
case HM.lookup qi tableMap of
|
||||
Nothing -> Left (TableNotFound qiSchema qiName (HM.elems tableMap))
|
||||
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName (HM.elems tableMap)
|
||||
Just _ -> Right qi
|
||||
|
||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addFilters ctx ApiRequest{..} rReq =
|
||||
foldr addFilterToNode (Right rReq) flts
|
||||
where
|
||||
@@ -806,16 +806,16 @@ addFilters ctx ApiRequest{..} rReq =
|
||||
ActDb (ActRoutine _ _) -> qsFilters
|
||||
_ -> qsFiltersNotRoot
|
||||
|
||||
addFilterToNode :: (EmbedPath, Filter) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addFilterToNode =
|
||||
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
||||
|
||||
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addOrders ctx ApiRequest{..} rReq = foldr addOrderToNode (Right rReq) qsOrder
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
|
||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
|
||||
|
||||
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
||||
@@ -825,7 +825,7 @@ resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUn
|
||||
-- Validates that the related resource on the order is an embedded resource,
|
||||
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
||||
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
||||
addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRelatedOrders :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
newOrder <- newRelOrder `traverse` order
|
||||
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
||||
@@ -839,9 +839,9 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
name = fromMaybe relName relAlias in
|
||||
if isToOne == Just True
|
||||
then Right $ cot{coRelation=relAggAlias}
|
||||
else Left $ RelatedOrderNotToOne (qiName from) name
|
||||
else Left $ ApiRequestError $ RelatedOrderNotToOne (qiName from) name
|
||||
Nothing ->
|
||||
Left $ NotEmbedded coRelation
|
||||
Left $ ApiRequestError $ NotEmbedded coRelation
|
||||
|
||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||
--
|
||||
@@ -910,13 +910,13 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
--
|
||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nonNullOp subForestPlan)
|
||||
-- Right [CoercibleStmnt (CoercibleFilterNullEmbed False "clients_projects_1")]
|
||||
addNullEmbedFilters :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addNullEmbedFilters :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
||||
let forestReadPlans = rootLabel <$> forest
|
||||
newLogic <- newNullFilters forestReadPlans `traverse` curLogic
|
||||
Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)
|
||||
where
|
||||
newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either ApiRequestError CoercibleLogicTree
|
||||
newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either Error CoercibleLogicTree
|
||||
newNullFilters rPlans = \case
|
||||
(CoercibleExpr b lOp trees) ->
|
||||
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
||||
@@ -928,25 +928,25 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
||||
flt@(CoercibleStmnt _) ->
|
||||
Right flt
|
||||
|
||||
addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRanges :: ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRanges ApiRequest{..} rReq =
|
||||
case iAction of
|
||||
ActDb (ActRelationMut _ _) -> Right rReq
|
||||
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
||||
where
|
||||
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
|
||||
ranges = first QueryParamError $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
ranges :: Either Error [(EmbedPath, NonnegRange)]
|
||||
ranges = first (ApiRequestError . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
|
||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||
|
||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTrees ctx ApiRequest{..} rReq =
|
||||
foldr addLogicTreeToNode (Right rReq) qsLogic
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
|
||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
||||
|
||||
resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree
|
||||
@@ -957,12 +957,12 @@ resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
||||
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}
|
||||
|
||||
-- Find a Node of the Tree and apply a function to it
|
||||
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
updateNode f ([], a) rr = f a <$> rr
|
||||
updateNode _ _ (Left e) = Left e
|
||||
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||
case findNode of
|
||||
Nothing -> Left $ NotEmbedded targetNodeName
|
||||
Nothing -> Left $ ApiRequestError $ NotEmbedded targetNodeName
|
||||
Just target ->
|
||||
(\node -> Node rootNode $ node : delete target forest) <$>
|
||||
updateNode f (remainingPath, a) (Right target)
|
||||
@@ -971,7 +971,7 @@ updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
||||
|
||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq = mapLeft ApiRequestError $
|
||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
||||
case mutation of
|
||||
MutationCreate ->
|
||||
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
||||
@@ -986,7 +986,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
||||
_ -> False) qsFiltersRoot
|
||||
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
||||
else
|
||||
Left InvalidFilters
|
||||
Left $ ApiRequestError InvalidFilters
|
||||
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
||||
where
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
@@ -1006,9 +1006,9 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
||||
applyDefaults = preferMissing == Just ApplyDefaults
|
||||
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
|
||||
|
||||
resolveOrError :: ResolverContext -> Table -> FieldName -> Either ApiRequestError CoercibleField
|
||||
resolveOrError :: ResolverContext -> Table -> FieldName -> Either Error CoercibleField
|
||||
resolveOrError ctx table field = case resolveTableFieldName table field Nothing of
|
||||
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
||||
CoercibleField{cfIRType=""} -> Left $ SchemaCacheErr $ ColumnNotFound (tableName table) field
|
||||
cf -> Right $ withJsonParse ctx cf
|
||||
|
||||
callPlan :: Routine -> ApiRequest -> S.Set FieldName -> CallArgs -> ReadPlanTree -> CallPlan
|
||||
|
||||
@@ -231,7 +231,7 @@ actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versio
|
||||
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
|
||||
case HM.lookup qi dbTables of
|
||||
Just tbl -> respondInfo $ allowH tbl
|
||||
Nothing -> Left $ Error.ApiRequestError $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
|
||||
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
|
||||
where
|
||||
allowH table =
|
||||
let hasPK = not . null $ tablePKCols table in
|
||||
|
||||
Reference in New Issue
Block a user