feat: data representations allow custom parsing and formatting of API fields.
See PR #2523. Most notable code changes: - Load data representation casts into schema cache. - Data representations for reads, filters, inserts, updates, views, over joins. - `CoercibleField` represents name references in queries where coercion may be needed. - `ResolverContext` help facilitate field resolution during planning. - Planner 'resolves' names in the API query and pairs them with any implicit conversions to be used in the query builder stage. - Tests for all of the above. - More consistent naming (TypedX -> CoercibleX). New: unit tests for more data representation use cases; helpful as examples as well. New: update CHANGELOG with data representations feature description. Fixed failing idempotence test. New: replace date formatter test with one that does something. Fixup: inadvertent CHANGELOG change after rebase. Cleanup: `tfName` -> `cfName` and related. Document what IRType means. Formatting. New: use a subquery to interpret `IN` literals requiring data rep transformation. - With the previous method, very long queries such as `ANY (ARRAY[test.color('000100'), test.color('CAFE12'), test.color('01E240'), ...` could be generated. Consider the case where the parser function name is 45 characters and there's a hundred literals. That's 4.5kB of SQL just for the function name alone! - New version uses `unnest`: `ANY (SELECT test.color(unnest('{000100,CAFE12,01E240,...}'::text[]))` to produce a much shorter query. - This is likely to be more performant and either way much more readable and debuggable in the logs.
This commit is contained in:
committed by
Steve Chavez
parent
078c6ec08c
commit
0a1564ba5a
+195
-72
@@ -25,9 +25,9 @@ module PostgREST.Plan
|
||||
, inspectPlanTxMode
|
||||
) where
|
||||
|
||||
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
||||
import qualified Data.List as L
|
||||
import qualified Data.Set as S
|
||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
||||
@@ -36,34 +36,39 @@ import Data.Either.Combinators (mapLeft, mapRight)
|
||||
import Data.List (delete)
|
||||
import Data.Tree (Tree (..))
|
||||
|
||||
import PostgREST.ApiRequest (Action (..),
|
||||
ApiRequest (..),
|
||||
InvokeMethod (..),
|
||||
Mutation (..),
|
||||
Payload (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
restrictRange)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..),
|
||||
RelationshipsMap,
|
||||
relIsToOne)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap,
|
||||
RoutineParam (..),
|
||||
funcReturnsCompositeAlias,
|
||||
funcReturnsScalar,
|
||||
funcReturnsSetOfScalar)
|
||||
import PostgREST.SchemaCache.Table (Table (tableName),
|
||||
tablePKCols)
|
||||
import PostgREST.ApiRequest (Action (..),
|
||||
ApiRequest (..),
|
||||
InvokeMethod (..),
|
||||
Mutation (..),
|
||||
Payload (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
restrictRange)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..),
|
||||
RelationshipsMap,
|
||||
relIsToOne)
|
||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||
RepresentationsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||
RoutineMap,
|
||||
RoutineParam (..),
|
||||
funcReturnsCompositeAlias,
|
||||
funcReturnsScalar,
|
||||
funcReturnsSetOfScalar)
|
||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||
TablesMap,
|
||||
tableColumnsList,
|
||||
tablePKCols)
|
||||
|
||||
import PostgREST.ApiRequest.Preferences
|
||||
import PostgREST.ApiRequest.Types
|
||||
@@ -197,26 +202,93 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPo
|
||||
inspectPlanTxMode :: SQL.Mode
|
||||
inspectPlanTxMode = SQL.Read
|
||||
|
||||
-- | During planning we need to resolve Field -> CoercibleField (finding the context specific target type and map function).
|
||||
-- | ResolverContext facilitates this without the need to pass around a laundry list of parameters.
|
||||
data ResolverContext = ResolverContext
|
||||
{ tables :: TablesMap
|
||||
, representations :: RepresentationsMap
|
||||
, qi :: QualifiedIdentifier -- ^ The table we're currently attending; changes as we recurse into joins etc.
|
||||
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
||||
}
|
||||
|
||||
resolveColumnField :: Column -> CoercibleField
|
||||
resolveColumnField col = CoercibleField (colName col) mempty (colNominalType col) Nothing (colDefault col)
|
||||
|
||||
resolveTableFieldName :: Table -> FieldName -> CoercibleField
|
||||
resolveTableFieldName table fieldName =
|
||||
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
||||
Just . resolveColumnField
|
||||
|
||||
resolveTableField :: Table -> Field -> CoercibleField
|
||||
resolveTableField table (fieldName, []) = resolveTableFieldName table fieldName
|
||||
-- If the field is known and a JSON path is given, always assume the JSON type. But don't assume a type for entirely unknown fields.
|
||||
resolveTableField table (fieldName, jp) =
|
||||
case resolveTableFieldName table fieldName of
|
||||
cf@CoercibleField{cfIRType=""} -> cf{cfJsonPath=jp}
|
||||
cf -> cf{cfJsonPath=jp, cfIRType="json"}
|
||||
|
||||
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
||||
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
||||
resolveTypeOrUnknown ResolverContext{..} field@(fn, jp) =
|
||||
fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
||||
Just . flip resolveTableField field
|
||||
|
||||
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
||||
--
|
||||
-- Note that we change the IR type here. This might seem unintuitive. The short of it is that for a CoercibleField without a transformer, input type == output type. A transformer maps from a -> b, so by definition the input type will be a and the output type b after. And cfIRType is the *input* type.
|
||||
--
|
||||
-- It might feel odd that once a transformer is added we 'forget' the target type (because now a /= b). You might also note there's no obvious way to stack transforms (even if there was a stack, you erased what type you're working with so it's awkward). Alas as satisfying as it would be to engineer a layered mapping system with full type information, we just don't need it.
|
||||
withTransformer :: ResolverContext -> Text -> Text -> CoercibleField -> CoercibleField
|
||||
withTransformer ResolverContext{representations} sourceType targetType field =
|
||||
fromMaybe field $ HM.lookup (sourceType, targetType) representations >>=
|
||||
(\fieldRepresentation -> Just field{cfIRType=sourceType, cfTransform=Just (drFunction fieldRepresentation)})
|
||||
|
||||
-- | Map the intermediate representation type to the output type, if available.
|
||||
withOutputFormat :: ResolverContext -> CoercibleField -> CoercibleField
|
||||
withOutputFormat ctx@ResolverContext{outputType} field@CoercibleField{cfIRType} = withTransformer ctx cfIRType outputType field
|
||||
|
||||
-- | Map text into the intermediate representation type, if available.
|
||||
withTextParse :: ResolverContext -> CoercibleField -> CoercibleField
|
||||
withTextParse ctx field@CoercibleField{cfIRType} = withTransformer ctx "text" cfIRType field
|
||||
|
||||
-- | Map json into the intermediate representation type, if available.
|
||||
withJsonParse :: ResolverContext -> CoercibleField -> CoercibleField
|
||||
withJsonParse ctx field@CoercibleField{cfIRType} = withTransformer ctx "json" cfIRType field
|
||||
|
||||
-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.
|
||||
resolveOutputField :: ResolverContext -> Field -> CoercibleField
|
||||
resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field
|
||||
|
||||
-- | Map the query string format of a value (text) into the intermediate representation type, if available.
|
||||
resolveQueryInputField :: ResolverContext -> Field -> CoercibleField
|
||||
resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx field
|
||||
|
||||
-- | Builds the ReadPlan tree on a number of stages.
|
||||
-- | Adds filters, order, limits on its respective nodes.
|
||||
-- | Adds joins conditions obtained from resource embedding.
|
||||
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbRelationships} apiRequest =
|
||||
mapLeft ApiRequestError $
|
||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||
addNullEmbedFilters =<<
|
||||
validateSpreadEmbeds =<<
|
||||
addRelatedOrders =<<
|
||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||
addLogicTrees apiRequest =<<
|
||||
addRanges apiRequest =<<
|
||||
addOrders apiRequest =<<
|
||||
addFilters apiRequest (initReadRequest qi $ QueryParams.qsSelect $ iQueryParams apiRequest)
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
||||
let
|
||||
-- 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) =<<
|
||||
addNullEmbedFilters =<<
|
||||
validateSpreadEmbeds =<<
|
||||
addRelatedOrders =<<
|
||||
addDataRepresentationAliases =<<
|
||||
expandStarsForDataRepresentations ctx =<<
|
||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||
addLogicTrees ctx apiRequest =<<
|
||||
addRanges apiRequest =<<
|
||||
addOrders apiRequest =<<
|
||||
addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
|
||||
|
||||
-- Build the initial read plan tree
|
||||
initReadRequest :: QualifiedIdentifier -> [Tree SelectItem] -> ReadPlanTree
|
||||
initReadRequest qi@QualifiedIdentifier{..} =
|
||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi, relName=qiName, depth=rootDepth} []
|
||||
initReadRequest :: ResolverContext -> [Tree SelectItem] -> ReadPlanTree
|
||||
initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
|
||||
where
|
||||
rootDepth = 0
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False rootDepth
|
||||
@@ -235,7 +307,49 @@ initReadRequest qi@QualifiedIdentifier{..} =
|
||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
|
||||
fldForest:rForest
|
||||
SelectField{..} ->
|
||||
Node q{select=(selField, selCast, selAlias):select q} rForest
|
||||
Node q{select=(resolveOutputField ctx{qi=from q} selField, selCast, selAlias):select q} rForest
|
||||
|
||||
-- | Preserve the original field name if data representation is used to coerce the value.
|
||||
addDataRepresentationAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addDataRepresentationAliases rPlanTree = Right $ fmap (\rPlan@ReadPlan{select=sel} -> rPlan{select=map aliasSelectItem sel}) rPlanTree
|
||||
where
|
||||
aliasSelectItem :: (CoercibleField, Maybe Cast, Maybe Alias) -> (CoercibleField, Maybe Cast, Maybe Alias)
|
||||
-- If there already is an alias, don't overwrite it.
|
||||
aliasSelectItem (fld@(CoercibleField{cfName=fieldName, cfTransform=(Just _)}), Nothing, Nothing) = (fld, Nothing, Just fieldName)
|
||||
aliasSelectItem fld = fld
|
||||
|
||||
knownColumnsInContext :: ResolverContext -> [Column]
|
||||
knownColumnsInContext ResolverContext{..} =
|
||||
fromMaybe [] $ HM.lookup qi tables >>=
|
||||
Just . tableColumnsList
|
||||
|
||||
-- | Expand "select *" into explicit field names of the table, if necessary to apply data representations.
|
||||
expandStarsForDataRepresentations :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
expandStarsForDataRepresentations ctx@ResolverContext{qi} rPlanTree = Right $ fmap expandStars rPlanTree
|
||||
where
|
||||
expandStars :: ReadPlan -> ReadPlan
|
||||
-- When the schema is "" and the table is the source CTE, we assume the true source table is given in the from
|
||||
-- alias and belongs to the request schema. See the bit in `addRels` with `newFrom = ...`.
|
||||
expandStars rPlan@ReadPlan{from=(QualifiedIdentifier "" "pgrst_source"), fromAlias=(Just tblAlias)} =
|
||||
expandStarsForTable ctx{qi=qi{qiName=tblAlias}} rPlan
|
||||
expandStars rPlan@ReadPlan{from=fromTable} =
|
||||
expandStarsForTable ctx{qi=fromTable} rPlan
|
||||
|
||||
expandStarsForTable :: ResolverContext -> ReadPlan -> ReadPlan
|
||||
expandStarsForTable ctx@ResolverContext{representations, outputType} rplan@ReadPlan{select=selectItems} =
|
||||
-- If we have a '*' select AND the target table has at least one data representation, expand.
|
||||
if ("*" `elem` map (\(field, _, _) -> cfName field) selectItems) && any hasOutputRep knownColumns
|
||||
then rplan{select=concatMap (expandStarSelectItem knownColumns) selectItems}
|
||||
else rplan
|
||||
where
|
||||
knownColumns = knownColumnsInContext ctx
|
||||
|
||||
hasOutputRep :: Column -> Bool
|
||||
hasOutputRep col = HM.member (colNominalType col, outputType) representations
|
||||
|
||||
expandStarSelectItem :: [Column] -> (CoercibleField, Maybe Cast, Maybe Alias) -> [(CoercibleField, Maybe Cast, Maybe Alias)]
|
||||
expandStarSelectItem columns (CoercibleField{cfName="*", cfJsonPath=[]}, b, c) = map (\col -> (withOutputFormat ctx $ resolveColumnField col, b, c)) columns
|
||||
expandStarSelectItem _ selectItem = [selectItem]
|
||||
|
||||
-- | Enforces the `max-rows` config on the result
|
||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
@@ -390,8 +504,8 @@ findRel schema allRels origin target hint =
|
||||
)
|
||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
|
||||
|
||||
addFilters :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addFilters ApiRequest{..} rReq =
|
||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addFilters ctx ApiRequest{..} rReq =
|
||||
foldr addFilterToNode (Right rReq) flts
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
@@ -403,7 +517,7 @@ addFilters ApiRequest{..} rReq =
|
||||
|
||||
addFilterToNode :: (EmbedPath, Filter) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addFilterToNode =
|
||||
updateNode (\flt (Node q@ReadPlan{where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest flt lf} f)
|
||||
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
||||
|
||||
addOrders :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addOrders ApiRequest{..} rReq =
|
||||
@@ -447,15 +561,15 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=oldLogic} forest) = do
|
||||
newLogic <- getFilters readPlans `traverse` oldLogic
|
||||
Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)
|
||||
where
|
||||
getFilters :: [ReadPlan] -> LogicTree -> Either ApiRequestError LogicTree
|
||||
getFilters rPlans (Expr b lOp trees) = Expr b lOp <$> (getFilters rPlans `traverse` trees)
|
||||
getFilters rPlans flt@(Stmnt (Filter (fld, []) opExpr)) =
|
||||
getFilters :: [ReadPlan] -> CoercibleLogicTree -> Either ApiRequestError CoercibleLogicTree
|
||||
getFilters rPlans (CoercibleExpr b lOp trees) = CoercibleExpr b lOp <$> (getFilters rPlans `traverse` trees)
|
||||
getFilters rPlans flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _) opExpr)) =
|
||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
||||
case (foundRP, opExpr) of
|
||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ Stmnt $ FilterNullEmbed b relAggAlias
|
||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
||||
(Just ReadPlan{relName}, _) -> Left $ UnacceptableFilter relName
|
||||
_ -> Right flt
|
||||
getFilters _ flt@(Stmnt _) = Right flt
|
||||
getFilters _ flt@(CoercibleStmnt _) = Right flt
|
||||
|
||||
addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRanges ApiRequest{..} rReq =
|
||||
@@ -469,14 +583,22 @@ addRanges ApiRequest{..} rReq =
|
||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||
|
||||
addLogicTrees :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addLogicTrees ApiRequest{..} rReq =
|
||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addLogicTrees ctx ApiRequest{..} rReq =
|
||||
foldr addLogicTreeToNode (Right rReq) qsLogic
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
|
||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{where_=lf} f) -> Node q{ReadPlan.where_=t:lf} f)
|
||||
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
|
||||
resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt
|
||||
resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)
|
||||
|
||||
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
||||
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld, opExpr=opExpr}
|
||||
resolveFilter _ (FilterNullEmbed isNot fieldName) = CoercibleFilterNullEmbed isNot fieldName
|
||||
|
||||
-- Validates that spread embeds are only done on to-one relationships
|
||||
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
@@ -502,7 +624,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{..}, ..} sCache readReq = mapLeft ApiRequestError $
|
||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq = mapLeft ApiRequestError $
|
||||
case mutation of
|
||||
MutationCreate ->
|
||||
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
||||
@@ -520,27 +642,28 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} sCache readR
|
||||
Left InvalidFilters
|
||||
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
|
||||
where
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
confCols = fromMaybe pkCols qsOnConflict
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
returnings =
|
||||
if preferRepresentation == None
|
||||
then []
|
||||
else inferColsEmbedNeeds readReq pkCols
|
||||
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables sCache
|
||||
logic = map snd qsLogic
|
||||
tbl = HM.lookup qi dbTables
|
||||
pkCols = maybe mempty tablePKCols tbl
|
||||
logic = map (resolveLogicTree ctx . snd) qsLogic
|
||||
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
||||
combinedLogic = foldr addFilterToLogicForest logic qsFiltersRoot
|
||||
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
||||
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||
tbl = HM.lookup qi $ dbTables sCache
|
||||
typedColumnsOrError = resolveOrError tbl `traverse` S.toList iColumns
|
||||
applyDefaults = preferMissing == Just ApplyDefaults
|
||||
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
|
||||
|
||||
resolveOrError :: Maybe Table -> FieldName -> Either ApiRequestError TypedField
|
||||
resolveOrError Nothing _ = Left NotFound
|
||||
resolveOrError (Just table) field =
|
||||
case resolveTableField table field of
|
||||
Nothing -> Left $ ColumnNotFound (tableName table) field
|
||||
Just typedField -> Right typedField
|
||||
resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
|
||||
resolveOrError _ Nothing _ = Left NotFound
|
||||
resolveOrError ctx (Just table) field =
|
||||
case resolveTableFieldName table field of
|
||||
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
||||
cf -> Right $ withJsonParse ctx cf
|
||||
|
||||
callPlan :: Routine -> ApiRequest -> S.Set FieldName -> LBS.ByteString -> ReadPlanTree -> CallPlan
|
||||
callPlan proc ApiRequest{iPreferences=Preferences{..}} paramKeys args readReq = FunctionCall {
|
||||
@@ -569,7 +692,7 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
| "*" `elem` fldNames = ["*"]
|
||||
| otherwise = returnings
|
||||
where
|
||||
fldNames = (\((fld, _), _, _) -> fld) <$> select
|
||||
fldNames = cfName . (\(f, _, _) -> f) <$> select
|
||||
-- Without fkCols, when a mutatePlan to
|
||||
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
||||
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
||||
@@ -608,8 +731,8 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
|
||||
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
||||
-- they are later concatenated with AND in the QueryBuilder
|
||||
addFilterToLogicForest :: Filter -> [LogicTree] -> [LogicTree]
|
||||
addFilterToLogicForest flt lf = Stmnt flt : lf
|
||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
||||
|
||||
-- | If raw(binary) output is requested, check that MediaType is one of the
|
||||
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
||||
@@ -638,6 +761,6 @@ binaryField AppConfig{configRawMediaTypes} acceptMediaType proc rpTree
|
||||
_ -> False
|
||||
|
||||
fstFieldName :: ReadPlanTree -> Maybe FieldName
|
||||
fstFieldName (Node ReadPlan{select=(("*", []), _, _):_} []) = Nothing
|
||||
fstFieldName (Node ReadPlan{select=[((fld, []), _, _)]} []) = Just fld
|
||||
fstFieldName (Node ReadPlan{select=(CoercibleField{cfName="*", cfJsonPath=[]}, _, _):_} []) = Nothing
|
||||
fstFieldName (Node ReadPlan{select=[(CoercibleField{cfName=fld, cfJsonPath=[]}, _, _)]} []) = Just fld
|
||||
fstFieldName _ = Nothing
|
||||
|
||||
Reference in New Issue
Block a user