From 3e6f9f91b4151971953927d1ea3f50a5295def27 Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi Date: Wed, 15 Apr 2020 15:04:24 +0530 Subject: [PATCH 01/29] [skip ci] generate relay schema --- .../src-lib/Hasura/GraphQL/Resolve/Types.hs | 2 + server/src-lib/Hasura/GraphQL/Schema.hs | 21 +++- .../src-lib/Hasura/GraphQL/Schema/Common.hs | 8 ++ .../src-lib/Hasura/GraphQL/Schema/Function.hs | 17 +++ .../src-lib/Hasura/GraphQL/Schema/Select.hs | 104 +++++++++++++++++- 5 files changed, 146 insertions(+), 6 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs index d8fdcbd067f5e..3dda96769d2ed 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs @@ -29,10 +29,12 @@ import qualified Hasura.SQL.DML as S data QueryCtx = QCSelect !SelOpCtx + | QCSelectConnection !SelOpCtx | QCSelectPkey !SelPkOpCtx | QCSelectAgg !SelOpCtx | QCFuncQuery !FuncQOpCtx | QCFuncAggQuery !FuncQOpCtx + | QCFuncConnection !FuncQOpCtx | QCActionFetch !ActionSelectOpContext deriving (Show, Eq) diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index 151e5602d8c13..0126f01838fb6 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -154,10 +154,10 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi <> queryTypes <> aggQueryTypes <> mutationTypes <> funcInpArgTys <> referencedEnumTypes <> computedFieldFuncArgsInps - queryTypes = catMaybes + queryTypes = map TIObj selectObjects <> + catMaybes [ TIInpObj <$> boolExpInpObjM , TIInpObj <$> ordByInpObjM - , TIObj <$> selObjM ] aggQueryTypes = map TIObj aggObjs <> map TIInpObj aggOrdByInps @@ -254,7 +254,13 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi && any (`isMutable` viM) [viIsInsertable, viIsUpdatable, viIsDeletable] -- table obj - selObjM = mkTableObj tn descM <$> selFldsM + selectObjects = case selPermM of + Just (_, selFlds) -> + [ mkTableObj tn descM selFlds + , mkTableEdgeObj tn + , mkTableConnectionObj tn + ] + Nothing -> [] -- aggregate objs and order by inputs (aggObjs, aggOrdByInps) = case selPermM of @@ -339,9 +345,11 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM RootFields { _rootQueryFields = makeFieldMap $ funcQueries + <> funcConnectionQueries <> funcAggQueries <> catMaybes [ getSelDet <$> selM + , getSelConnectionDet <$> selM , getSelAggDet selM , getPKeySelDet <$> selM <*> primaryKey ] @@ -360,6 +368,7 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM colGNameMap = mkPGColGNameMap $ getCols fields funcQueries = maybe [] getFuncQueryFlds selM + funcConnectionQueries = maybe [] getFuncQueryConnectionFlds selM funcAggQueries = maybe [] getFuncAggQueryFlds selM mutHelper :: (ViewInfo -> Bool) -> (a -> b) -> Maybe a -> Maybe b @@ -411,6 +420,8 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM selCustName = getCustomNameWith _tcrfSelect getSelDet (selFltr, pLimit, hdrs, _) = selFldHelper QCSelect (mkSelFld selCustName) selFltr pLimit hdrs + getSelConnectionDet (selFltr, pLimit, hdrs, _) = + selFldHelper QCSelectConnection (mkSelFldConnection Nothing) selFltr pLimit hdrs selAggCustName = getCustomNameWith _tcrfSelectAggregate getSelAggDet (Just (selFltr, pLimit, hdrs, True)) = @@ -433,6 +444,9 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM getFuncQueryFlds (selFltr, pLimit, hdrs, _) = funcFldHelper QCFuncQuery mkFuncQueryFld selFltr pLimit hdrs + getFuncQueryConnectionFlds (selFltr, pLimit, hdrs, _) = + funcFldHelper QCFuncConnection mkFuncQueryConnectionFld selFltr pLimit hdrs + getFuncAggQueryFlds (selFltr, pLimit, hdrs, True) = funcFldHelper QCFuncAggQuery mkFuncAggQueryFld selFltr pLimit hdrs getFuncAggQueryFlds _ = [] @@ -802,6 +816,7 @@ mkGCtx tyAgg (RootFields queryFields mutationFields) insCtxMap = , TIObj <$> mutRootM , TIObj <$> subRootM , TIEnum <$> ordByEnumTyM + , Just $ TIObj mkPageInfoObj ] <> scalarTys <> compTys <> defaultTypes <> wiredInGeoInputTypes <> wiredInRastInputTypes diff --git a/server/src-lib/Hasura/GraphQL/Schema/Common.hs b/server/src-lib/Hasura/GraphQL/Schema/Common.hs index 31dcab5f16c68..bf3ede49f1ce6 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Common.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Common.hs @@ -16,6 +16,8 @@ module Hasura.GraphQL.Schema.Common , mkComputedFieldName , mkTableTy + , mkTableConnectionTy + , mkTableEdgeTy , mkTableEnumType , mkTableAggTy @@ -91,6 +93,12 @@ mkColumnType = \case mkTableTy :: QualifiedTable -> G.NamedType mkTableTy = G.NamedType . qualObjectToName +mkTableConnectionTy :: QualifiedTable -> G.NamedType +mkTableConnectionTy = addTypeSuffix "Connection" . mkTableTy + +mkTableEdgeTy :: QualifiedTable -> G.NamedType +mkTableEdgeTy = addTypeSuffix "Edge" . mkTableTy + mkTableEnumType :: QualifiedTable -> G.NamedType mkTableEnumType = addTypeSuffix "_enum" . mkTableTy diff --git a/server/src-lib/Hasura/GraphQL/Schema/Function.hs b/server/src-lib/Hasura/GraphQL/Schema/Function.hs index f3349635069e6..79b4212eb509b 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Function.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Function.hs @@ -2,6 +2,7 @@ module Hasura.GraphQL.Schema.Function ( procFuncArgs , mkFuncArgsInp , mkFuncQueryFld + , mkFuncQueryConnectionFld , mkFuncAggQueryFld , mkFuncArgsTy ) where @@ -92,6 +93,20 @@ mkFuncQueryFld funInfo descM = ty = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy retTable +mkFuncQueryConnectionFld + :: FunctionInfo -> Maybe PGDescription -> ObjFldInfo +mkFuncQueryConnectionFld funInfo descM = + mkHsraObjFldInfo (Just desc) fldName (mkFuncArgs funInfo) ty + where + retTable = fiReturnType funInfo + funcName = fiName funInfo + + desc = mkDescriptionWith descM $ "execute function " <> funcName + <<> " which returns " <>> retTable + fldName = qualObjectToName funcName <> "_connection" + + ty = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableConnectionTy retTable + {- function_aggregate( @@ -118,3 +133,5 @@ mkFuncAggQueryFld funInfo descM = fldName = qualObjectToName funcName <> "_aggregate" ty = G.toGT $ G.toNT $ mkTableAggTy retTable + + diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index a3179466a53ec..1ded33b5cd429 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -4,10 +4,15 @@ module Hasura.GraphQL.Schema.Select , mkSelColumnTy , mkTableAggFldsObj , mkTableColAggFldsObj + , mkTableEdgeObj + , mkPageInfoObj + , mkTableConnectionObj + , mkTableConnectionTy , mkSelFld , mkAggSelFld , mkSelFldPKey + , mkSelFldConnection , mkSelArgs ) where @@ -138,7 +143,7 @@ mkRelationshipField -> Bool -> [ObjFldInfo] mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) isNullable = case rTy of - ArrRel -> bool [arrRelFld] [arrRelFld, aggArrRelFld] allowAgg + ArrRel -> bool [arrRelFld] [arrRelFld, arrConnectionFld, aggArrRelFld] allowAgg ObjRel -> [objRelFld] where objRelFld = mkHsraObjFldInfo (Just "An object relationship") @@ -149,8 +154,14 @@ mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) isNullable = cas arrRelFld = mkHsraObjFldInfo (Just "An array relationship") (mkRelName rn) - (fromInpValL $ mkSelArgs remTab) arrRelTy - arrRelTy = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy remTab + (fromInpValL $ mkSelArgs remTab) $ + G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy remTab + + arrConnectionFld = + mkHsraObjFldInfo Nothing (mkRelName rn <> "_connection") + (fromInpValL $ mkSelArgs remTab) $ + G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableConnectionTy remTab + aggArrRelFld = mkHsraObjFldInfo (Just "An aggregated array relationship") (mkAggRelName rn) (fromInpValL $ mkSelArgs remTab) $ G.toGT $ G.toNT $ mkTableAggTy remTab @@ -280,6 +291,93 @@ mkSelFld mCustomName tn = args = fromInpValL $ mkSelArgs tn ty = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy tn +{- + +table( + where: table_bool_exp + limit: Int + offset: Int +): [tableConnection!]! + +-} + +mkSelFldConnection :: Maybe G.Name -> QualifiedTable -> ObjFldInfo +mkSelFldConnection mCustomName tn = + mkHsraObjFldInfo (Just desc) fldName args ty + where + desc = G.Description $ "fetch data from the table: " <>> tn + fldName = fromMaybe (qualObjectToName tn <> "_connection") mCustomName + args = fromInpValL $ mkSelArgs tn + ty = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableConnectionTy tn + +{- +type tableConnection { + pageInfo: PageInfo! + edges: [tableEdge!]! +} +-} +mkTableConnectionObj + :: QualifiedTable -> ObjTyInfo +mkTableConnectionObj tn = + mkHsraObjTyInfo (Just desc) (mkTableConnectionTy tn) Set.empty $ mapFromL _fiName + [pageInfoFld, edgesFld] + where + desc = G.Description $ "A Relay Connection object on " <>> tn + pageInfoFld = mkHsraObjFldInfo Nothing "pageInfo" Map.empty $ + G.toGT $ G.toNT $ pageInfoTy + edgesFld = mkHsraObjFldInfo Nothing "edges" Map.empty $ G.toGT $ + G.toNT $ G.toLT $ G.toNT $ mkTableEdgeTy tn + +booleanScalar :: G.NamedType +booleanScalar = G.NamedType "Boolean" + +stringScalar :: G.NamedType +stringScalar = G.NamedType "String" + +pageInfoTyName :: G.Name +pageInfoTyName = "PageInfo" + +pageInfoTy :: G.NamedType +pageInfoTy = G.NamedType pageInfoTyName +{- +type PageInfo { + hasNextPage: Boolean! + hasPrevousPage: Boolean! + startCursor: String! + endCursor: String! +} +-} +mkPageInfoObj :: ObjTyInfo +mkPageInfoObj = + mkHsraObjTyInfo Nothing pageInfoTy Set.empty $ mapFromL _fiName + [hasNextPage, hasPreviousPage, startCursor, endCursor] + where + hasNextPage = mkHsraObjFldInfo Nothing "hasNextPage" Map.empty $ + G.toGT $ G.toNT booleanScalar + hasPreviousPage = mkHsraObjFldInfo Nothing "hasPreviousPage" Map.empty $ + G.toGT $ G.toNT booleanScalar + startCursor = mkHsraObjFldInfo Nothing "startCursor" Map.empty $ + G.toGT $ G.toNT stringScalar + endCursor = mkHsraObjFldInfo Nothing "endCursor" Map.empty $ + G.toGT $ G.toNT stringScalar + +{- +type tableConnection { + cursor: String! + node: table +} +-} +mkTableEdgeObj + :: QualifiedTable -> ObjTyInfo +mkTableEdgeObj tn = + mkHsraObjTyInfo Nothing (mkTableEdgeTy tn) Set.empty $ mapFromL _fiName + [cursor, node] + where + cursor = mkHsraObjFldInfo Nothing "cursor" Map.empty $ + G.toGT $ G.toNT stringScalar + node = mkHsraObjFldInfo Nothing "node" Map.empty $ G.toGT $ + mkTableTy tn + {- table_by_pk( col1: value1!, From 615fccb4156af3f901667ca3d51d3a79930eeef5 Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi Date: Fri, 17 Apr 2020 19:50:19 +0530 Subject: [PATCH 02/29] [skip ci] wip: query generation logic for Relay Connection spec --- server/src-lib/Hasura/GraphQL/Resolve.hs | 7 + .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 115 +++++++++++++- .../src-lib/Hasura/GraphQL/Schema/Select.hs | 30 +++- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 150 ++++++++++++++++++ server/src-lib/Hasura/RQL/DML/Select/Types.hs | 79 +++++++-- server/src-lib/Hasura/RQL/Types/BoolExp.hs | 8 + server/src-lib/Hasura/RQL/Types/Column.hs | 1 + server/src-lib/Hasura/RQL/Types/Common.hs | 1 + server/src-lib/Hasura/RQL/Types/DML.hs | 5 +- server/src-lib/Hasura/SQL/DML.hs | 47 ++++-- server/src-lib/Hasura/SQL/Types.hs | 2 +- 11 files changed, 417 insertions(+), 28 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve.hs b/server/src-lib/Hasura/GraphQL/Resolve.hs index f2dc4d766b20d..3e4b03357d4e9 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve.hs @@ -44,6 +44,7 @@ data QueryRootFldAST v = QRFPk !(DS.AnnSimpleSelG v) | QRFSimple !(DS.AnnSimpleSelG v) | QRFAgg !(DS.AnnAggSelG v) + | QRFConnection !(DS.ConnectionSelect v) | QRFActionSelect !(DS.AnnSimpleSelG v) deriving (Show, Eq) @@ -59,6 +60,7 @@ traverseQueryRootFldAST f = \case QRFPk s -> QRFPk <$> DS.traverseAnnSimpleSel f s QRFSimple s -> QRFSimple <$> DS.traverseAnnSimpleSel f s QRFAgg s -> QRFAgg <$> DS.traverseAnnAggSel f s + QRFConnection s -> QRFConnection <$> DS.traverseConnectionSelect f s QRFActionSelect s -> QRFActionSelect <$> DS.traverseAnnSimpleSel f s toPGQuery :: QueryRootFldResolved -> Q.Query @@ -67,6 +69,7 @@ toPGQuery = \case QRFSimple s -> DS.selectQuerySQL DS.JASMultipleRows s QRFAgg s -> DS.selectAggQuerySQL s QRFActionSelect s -> DS.selectQuerySQL DS.JASSingleObject s + QRFConnection s -> Q.fromBuilder $ toSQL $ DS.mkConnectionSelect s validateHdrs :: (Foldable t, QErrM m) => UserInfo -> t Text -> m () @@ -104,6 +107,10 @@ queryFldToPGAST fld = do QRFAgg <$> RS.convertFuncQueryAgg ctx fld QCActionFetch ctx -> QRFActionSelect <$> RA.resolveAsyncActionQuery userInfo ctx fld + QCSelectConnection ctx -> do + QRFConnection <$> RS.convertConnectionSelect ctx fld + QCFuncConnection ctx -> do + QRFConnection <$> RS.convertConnectionFuncQuery ctx fld mutFldToTx :: ( HasVersion diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index 704fc543f2eb5..5b9aaca18e0dc 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -1,5 +1,7 @@ module Hasura.GraphQL.Resolve.Select ( convertSelect + , convertConnectionSelect + , convertConnectionFuncQuery , convertSelectByPKey , convertAggSelect , convertFuncQuerySimple @@ -130,6 +132,53 @@ fromAggSelSet colGNameMap fldTy selSet = fmap toFields $ "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet G.Name t -> throw500 $ "unexpected field in _agg node: " <> t +fromConnectionSelSet + :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r + , Has OrdByCtx r, Has SQLGenCtx r + ) + => G.NamedType -> SelSet -> m (RS.ConnectionFields UnresolvedVal) +fromConnectionSelSet fldTy selSet = fmap toFields $ + withSelSet selSet $ \f -> do + let fTy = _fType f + fSelSet = _fSelSet f + case _fName f of + "__typename" -> return $ RS.ConnectionTypename $ G.unName $ G.unNamedType fldTy + "pageInfo" -> RS.ConnectionPageInfo <$> parsePageInfoSelectionSet fTy fSelSet + "edges" -> RS.ConnectionEdges <$> parseEdgeSelectionSet fTy fSelSet + -- "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet + -- "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet + G.Name t -> throw500 $ "unexpected field in _connection node: " <> t + +parseEdgeSelectionSet + :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r + , Has OrdByCtx r, Has SQLGenCtx r + ) + => G.NamedType -> SelSet -> m (RS.EdgeFields UnresolvedVal) +parseEdgeSelectionSet fldTy selSet = fmap toFields $ + withSelSet selSet $ \f -> do + let fTy = _fType f + fSelSet = _fSelSet f + case _fName f of + "__typename" -> pure $ RS.EdgeTypename $ G.unName $ G.unNamedType fldTy + "cursor" -> pure $ RS.EdgeCursor + "node" -> RS.EdgeNode <$> processTableSelectionSet fTy fSelSet + G.Name t -> throw500 $ "unexpected field in Edge node: " <> t + +parsePageInfoSelectionSet + :: ( MonadReusability m, MonadError QErr m) + => G.NamedType -> SelSet -> m RS.PageInfoFields +parsePageInfoSelectionSet fldTy selSet = + fmap toFields $ withSelSet selSet $ \f -> + case _fName f of + "__typename" -> pure $ RS.PageInfoTypename $ G.unName $ G.unNamedType fldTy + "hasNextPage" -> pure $ RS.PageInfoHasNextPage + "hasPreviousPage" -> pure $ RS.PageInfoHasPreviousPage + "startCursor" -> pure $ RS.PageInfoStartCursor + "endCursor" -> pure $ RS.PageInfoEndCursor + -- "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet + -- "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet + G.Name t -> throw500 $ "unexpected field in PageInfo node: " <> t + type TableArgs = RS.TableArgsG UnresolvedVal parseTableArgs @@ -344,14 +393,18 @@ convertSelectByPKey opCtx fld = SelPkOpCtx qt _ permFilter colArgMap = opCtx -- agg select related -parseColumns :: (MonadReusability m, MonadError QErr m) => PGColGNameMap -> AnnInpVal -> m [PGCol] +parseColumns + :: (MonadReusability m, MonadError QErr m) + => PGColGNameMap -> AnnInpVal -> m [PGCol] parseColumns allColFldMap val = flip withArray val $ \_ vals -> forM vals $ \v -> do (_, G.EnumValue enumVal) <- asEnumVal v pgiColumn <$> resolvePGCol allColFldMap enumVal -convertCount :: (MonadReusability m, MonadError QErr m) => PGColGNameMap -> ArgsMap -> m S.CountType +convertCount + :: (MonadReusability m, MonadError QErr m) + => PGColGNameMap -> ArgsMap -> m S.CountType convertCount colGNameMap args = do columnsM <- withArgM args "columns" $ parseColumns colGNameMap isDistinct <- or <$> withArgM args "distinct" parseDistinct @@ -419,6 +472,37 @@ fromAggField selectFrom colGNameMap permFilter permLimit fld = fieldAsPath fld $ where args = _fArguments fld +fromConnectionField + :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r + , Has OrdByCtx r, Has SQLGenCtx r + ) + => RS.SelectFromG UnresolvedVal + -> AnnBoolExpPartialSQL + -> Maybe Int + -> Field -> m (RS.ConnectionSelect UnresolvedVal) +fromConnectionField selectFrom permFilter permLimit fld = fieldAsPath fld $ do + tableArgs <- parseConnectionArgs args + aggSelFlds <- fromConnectionSelSet (_fType fld) (_fSelSet fld) + let unresolvedPermFltr = + fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter + let tabPerm = RS.TablePerm unresolvedPermFltr permLimit + strfyNum <- stringifyNum <$> asks getter + return $ RS.AnnSelG aggSelFlds selectFrom tabPerm tableArgs strfyNum + where + args = _fArguments fld + +parseConnectionArgs + :: ( MonadReusability m, MonadError QErr m, MonadReader r m + , Has FieldMap r, Has OrdByCtx r + ) + => ArgsMap -> m TableArgs +parseConnectionArgs args = do + whereExpM <- withArgM args "where" parseBoolExp + ordByExpML <- withArgM args "order_by" parseOrderBy + let ordByExpM = NE.nonEmpty =<< ordByExpML + -- limitExpM <- withArgM args "limit" parseLimit + return $ RS.TableArgs whereExpM ordByExpM Nothing Nothing Nothing + convertAggSelect :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r @@ -430,6 +514,17 @@ convertAggSelect opCtx fld = where SelOpCtx qt _ colGNameMap permFilter permLimit = opCtx +convertConnectionSelect + :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r + , Has OrdByCtx r, Has SQLGenCtx r + ) + => SelOpCtx -> Field -> m (RS.ConnectionSelect UnresolvedVal) +convertConnectionSelect opCtx fld = + withPathK "selectionSet" $ + fromConnectionField (RS.FromTable qt) permFilter permLimit fld + where + SelOpCtx qt _ _ permFilter permLimit = opCtx + parseFunctionArgs :: (MonadReusability m, MonadError QErr m) => Seq.Seq a @@ -512,3 +607,19 @@ convertFuncQueryAgg funcOpCtx fld = fromAggField selectFrom colGNameMap permFilter permLimit fld where FuncQOpCtx qf argSeq _ colGNameMap permFilter permLimit = funcOpCtx + +convertConnectionFuncQuery + :: ( MonadReusability m + , MonadError QErr m + , MonadReader r m + , Has FieldMap r + , Has OrdByCtx r + , Has SQLGenCtx r + ) + => FuncQOpCtx -> Field -> m (RS.ConnectionSelect UnresolvedVal) +convertConnectionFuncQuery funcOpCtx fld = + withPathK "selectionSet" $ fieldAsPath fld $ do + selectFrom <- makeFunctionSelectFrom qf argSeq fld + fromConnectionField selectFrom permFilter permLimit fld + where + FuncQOpCtx qf argSeq _ _ permFilter permLimit = funcOpCtx diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index 1ded33b5cd429..5ae4744e86d21 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -15,6 +15,7 @@ module Hasura.GraphQL.Schema.Select , mkSelFldConnection , mkSelArgs + , mkConnectionArgs ) where import qualified Data.HashMap.Strict as Map @@ -83,6 +84,7 @@ mkComputedFieldFld field = in (inputParams, G.toGT $ mkScalarTy scalarTy) CFTTable computedFieldtable -> let table = _cftTable computedFieldtable + -- TODO: connection stuff in ( fromInpValL $ maybeToList maybeFunctionInputArg <> mkSelArgs table , G.toGT $ G.toLT $ G.toNT $ mkTableTy table ) @@ -122,6 +124,30 @@ mkSelArgs tn = orderByDesc = "sort the rows by one or more columns" distinctDesc = "distinct select on columns" +-- distinct_on: [table_select_column!] +-- where: table_bool_exp +-- order_by: table_order_by +-- first: Int +-- after: String +-- last: Int +-- before: String +mkConnectionArgs :: QualifiedTable -> [InpValInfo] +mkConnectionArgs tn = + [ InpValInfo (Just whereDesc) "where" Nothing $ G.toGT $ mkBoolExpTy tn + , InpValInfo (Just orderByDesc) "order_by" Nothing $ G.toGT $ G.toLT $ G.toNT $ + mkOrdByTy tn + , InpValInfo (Just distinctDesc) "distinct_on" Nothing $ G.toGT $ G.toLT $ + G.toNT $ mkSelColumnInpTy tn + , InpValInfo Nothing "first" Nothing $ G.toGT $ mkScalarTy PGInteger + , InpValInfo Nothing "after" Nothing $ G.toGT $ mkScalarTy PGText + , InpValInfo Nothing "last" Nothing $ G.toGT $ mkScalarTy PGInteger + , InpValInfo Nothing "before" Nothing $ G.toGT $ mkScalarTy PGText + ] + where + whereDesc = "filter the rows returned" + orderByDesc = "sort the rows by one or more columns" + distinctDesc = "distinct select on columns" + {- array_relationship( @@ -159,7 +185,7 @@ mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) isNullable = cas arrConnectionFld = mkHsraObjFldInfo Nothing (mkRelName rn <> "_connection") - (fromInpValL $ mkSelArgs remTab) $ + (fromInpValL $ mkConnectionArgs remTab) $ G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableConnectionTy remTab aggArrRelFld = mkHsraObjFldInfo (Just "An aggregated array relationship") @@ -307,7 +333,7 @@ mkSelFldConnection mCustomName tn = where desc = G.Description $ "fetch data from the table: " <>> tn fldName = fromMaybe (qualObjectToName tn <> "_connection") mCustomName - args = fromInpValL $ mkSelArgs tn + args = fromInpValL $ mkConnectionArgs tn ty = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableConnectionTy tn {- diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index c6cd5b60121c0..360c5a4245108 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -1,6 +1,8 @@ module Hasura.RQL.DML.Select.Internal ( mkSQLSelect , mkAggSelect + -- , mkConnectionSelect + -- , JoinCandidate(..) , module Hasura.RQL.DML.Select.Types ) where @@ -426,6 +428,39 @@ aggSelToArrNode pfxs als aggSel = subQueryReq = hasAggFld aggFlds +-- connectionSelToArrNode +-- :: Prefixes -> FieldName -> ArrRelConnection S.SQLExp -> ArrNode +-- connectionSelToArrNode pfxs als aggSel = +-- ArrNode [extr] colMapping mergedBN +-- where +-- AnnRelG _ colMapping annSel = aggSel +-- AnnSelG aggFlds tabFrm tabPerm tabArgs strfyNum = annSel +-- fldAls = S.Alias $ toIden als + +-- extr = flip S.Extractor (Just fldAls) $ S.applyJsonBuildObj $ +-- concatMap selFldToExtr aggFlds + +-- permLimit = _tpLimit tabPerm +-- ordBy = _bnOrderBy mergedBN + +-- allBNs = map mkAggBaseNode aggFlds +-- emptyBN = mkEmptyBaseNode (_pfThis pfxs) tabFrm +-- mergedBN = foldr mergeBaseNodes emptyBN allBNs + +-- mkAggBaseNode (fn, selFld) = +-- mkBaseNode subQueryReq pfxs fn selFld tabFrm tabPerm tabArgs strfyNum + +-- selFldToExtr (FieldName t, fld) = (:) (S.SELit t) $ pure $ case fld of +-- TAFAgg flds -> aggFldToExp flds +-- TAFNodes _ -> +-- withJsonAggExtr subQueryReq permLimit ordBy $ S.Alias $ Iden t +-- TAFExp e -> +-- -- bool_or to force aggregation +-- S.SEFnApp "coalesce" +-- [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing + +-- subQueryReq = hasAggFld aggFlds + hasAggFld :: Foldable t => t (a, TableAggFldG v) -> Bool hasAggFld = any (isTabAggFld . snd) where @@ -534,6 +569,7 @@ mkBaseNode -> BaseNode mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom tablePerm tableArgs strfyNum = + BaseNode thisPfx distExprM fromItem finalWhere ordByExpM finalLimit offsetM allExtrs allObjsWithOb allArrsWithOb computedFields where @@ -682,6 +718,7 @@ mkArrNode subQueryReq pfxs (fldName, annArrSel) = case annArrSel of in ArrNode [extr] (aarMapping annArrRel) bn ASAgg annAggSel -> aggSelToArrNode pfxs fldName annAggSel + -- TODO: ASConnection connectionSel -> connectionSelToArrNode pfxs fldName connectionSel injectJoinCond :: S.BoolExp -- ^ Join condition -> S.BoolExp -- ^ Where condition @@ -754,6 +791,119 @@ baseNodeToSel joinCond baseNode = } in S.mkLateralFromItem sel als +-- mkConnectionSelect :: ConnectionSelect S.SQLExp -> S.Select +-- mkConnectionSelect annConnectionSel = +-- prefixNumToAliases $ arrNodeToSelect bn extr $ S.BELit True +-- where +-- connectionSelect = AnnRelG rootRelName HM.empty annConnectionSel +-- rootIden = Iden "root" +-- rootPrefix = Prefixes rootIden rootIden +-- ArrNode extr _ bn = +-- connectionSelToArrNode rootPrefix (FieldName "root") connectionSelect + +data TableSource + = TableSource + { _tsFrom :: !SelectFrom + , _tsPermission :: !TablePerm + , _tsArgs :: !TableArgs + } + deriving (Show, Eq, Generic) +instance Hashable TableSource + + +-- data TableSource +-- = TableSource +-- { _tsFrom :: !S.FromItem +-- , _tsAlias :: !S.Alias +-- , _tsWhere :: !S.BoolExp +-- , _tsLimit :: !(Maybe Int) +-- , _tsOffset :: !(Maybe S.SQLExp) +-- -- , _tsArgs :: !TableArgs +-- } +-- deriving (Show, Eq, Generic) + + +data BuildState + = BuildState + { _bsAliasGenerator :: !Int + , _bsJoinTree :: !JoinTree + } + +type Build = State BuildState + +-- | this is need in those cases where you need a single row when the table +-- returns multiple +forceAggregation :: S.SQLExp -> S.SQLExp +forceAggregation e = + S.SEFnApp "coalesce" [ e, S.SEUnsafe "bool_or('true')::text"] Nothing + +-- | Returns the SQL expression that will construct the response +-- for given field +buildConnectionFieldExpression + :: S.Alias + -- ^ The table alias + -> ConnectionField S.SQLExp + -> Build S.SQLExp +buildConnectionFieldExpression tableAlias = \case + ConnectionTypename typename -> pure $ forceAggregation $ S.SELit typename + ConnectionPageInfo fields -> + pure $ S.SELit "should be pageinfo" + ConnectionEdges fields -> do + fieldPairs <- forM fields $ \(fieldAlias, field) -> do + fieldExpression <- buildEdgeFieldExpression field + pure [S.SELit $ getFieldNameTxt fieldAlias, fieldExpression] + pure $ S.applyJsonBuildObj $ concat fieldPairs + where + buildEdgeFieldExpression = \case + EdgeTypename typename -> pure $ S.SELit typename + EdgeCursor -> pure $ S.SELit "will return a cursor" + EdgeNode fields -> undefined + +data Selector + = Selector !S.SQLExp !S.SQLExp + deriving (Show, Eq) + +-- | Returns the SQL expression that will construct the response +-- for given field +buildTableFieldExpression + :: S.Alias + -- ^ The table alias + -> AnnFldG S.SQLExp + -> Build S.SQLExp +buildTableFieldExpression tableAlias = \case + FCol field -> undefined + FObj field -> undefined + FArr field -> undefined + FComputedField field -> undefined + FExp typename -> pure $ S.SELit typename + +processFields + :: ConnectionFields S.SQLExp + -> Maybe (NE.NonEmpty (AnnOrderByItemG S.SQLExp)) + -> Build () +processFields = undefined + +mkSelectNode + :: ConnectionSelect S.SQLExp -> Build (TableSource, SelectNode) +mkSelectNode = undefined + +data SelectNode + = SelectNode + { _jnColumns :: !(HM.HashMap S.Alias S.SQLExp) + , _jnFrom :: !S.FromItem + , _jnAlias :: !S.Alias + , _jnJoinTree :: !JoinTree + , _jnWhere :: !S.BoolExp + , _jnOrderBy :: !(Maybe S.OrderByExp) + , _jnLimit :: !(Maybe Int) + , _jnOffset :: !(Maybe S.SQLExp) + } + deriving (Show, Eq) + +newtype JoinTree + = JoinTree { unJoinTree :: HM.HashMap TableSource SelectNode } + deriving (Show, Eq, Semigroup, Monoid) + mkAggSelect :: AnnAggSel -> S.Select mkAggSelect annAggSel = prefixNumToAliases $ arrNodeToSelect bn extr $ S.BELit True diff --git a/server/src-lib/Hasura/RQL/DML/Select/Types.hs b/server/src-lib/Hasura/RQL/DML/Select/Types.hs index 8dc04963043eb..a078eb0f814e9 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Types.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Types.hs @@ -53,13 +53,15 @@ instance FromJSON ExtCol where data AnnAggOrdBy = AAOCount | AAOOp !T.Text !PGCol - deriving (Show, Eq) + deriving (Show, Eq, Generic) +instance Hashable AnnAggOrdBy data AnnObColG v = AOCPG !PGCol | AOCObj !RelInfo !(AnnBoolExp v) !(AnnObColG v) | AOCAgg !RelInfo !(AnnBoolExp v) !AnnAggOrdBy - deriving (Show, Eq) + deriving (Show, Eq, Generic) +instance (Hashable v) => Hashable (AnnObColG v) traverseAnnObCol :: (Applicative f) @@ -99,6 +101,7 @@ type ObjSel = ObjSelG S.SQLExp type ArrRelG v = AnnRelG (AnnSimpleSelG v) type ArrRelAggG v = AnnRelG (AnnAggSelG v) +type ArrRelConnection v = AnnRelG (ConnectionSelect v) type ArrRelAgg = ArrRelAggG S.SQLExp data ComputedFieldScalarSel v @@ -127,6 +130,7 @@ type Fields a = [(FieldName, a)] data ArrSelG v = ASSimple !(ArrRelG v) | ASAgg !(ArrRelAggG v) + | ASConnection !(ArrRelConnection v) deriving (Show, Eq) traverseArrSel @@ -137,6 +141,8 @@ traverseArrSel traverseArrSel f = \case ASSimple arrRel -> ASSimple <$> traverse (traverseAnnSimpleSel f) arrRel ASAgg arrRelAgg -> ASAgg <$> traverse (traverseAnnAggSel f) arrRelAgg + ASConnection relConnection -> + ASConnection <$> traverse (traverseConnectionSelect f) relConnection type ArrSel = ArrSelG S.SQLExp @@ -193,7 +199,8 @@ data TableArgsG v , _taLimit :: !(Maybe Int) , _taOffset :: !(Maybe S.SQLExp) , _taDistCols :: !(Maybe (NE.NonEmpty PGCol)) - } deriving (Show, Eq) + } deriving (Show, Eq, Generic) +instance (Hashable v) => Hashable (TableArgsG v) traverseTableArgs :: (Applicative f) @@ -247,6 +254,46 @@ data TableAggFldG v | TAFExp !T.Text deriving (Show, Eq) +data PageInfoField + = PageInfoTypename !Text + | PageInfoHasNextPage + | PageInfoHasPreviousPage + | PageInfoStartCursor + | PageInfoEndCursor + deriving (Show, Eq) +type PageInfoFields = Fields PageInfoField + +data EdgeField v + = EdgeTypename !Text + | EdgeCursor + | EdgeNode !(AnnFldsG v) + deriving (Show, Eq) +type EdgeFields v = Fields (EdgeField v) + +traverseEdgeField + :: (Applicative f) + => (a -> f b) -> EdgeField a -> f (EdgeField b) +traverseEdgeField f = \case + EdgeTypename t -> pure $ EdgeTypename t + EdgeCursor -> pure EdgeCursor + EdgeNode fields -> EdgeNode <$> traverseAnnFlds f fields + +data ConnectionField v + = ConnectionTypename !Text + | ConnectionPageInfo !PageInfoFields + | ConnectionEdges !(EdgeFields v) + deriving (Show, Eq) +type ConnectionFields v = Fields (ConnectionField v) + +traverseConnectionField + :: (Applicative f) + => (a -> f b) -> ConnectionField a -> f (ConnectionField b) +traverseConnectionField f = \case + ConnectionTypename t -> pure $ ConnectionTypename t + ConnectionPageInfo fields -> pure $ ConnectionPageInfo fields + ConnectionEdges fields -> + ConnectionEdges <$> traverse (traverse (traverseEdgeField f)) fields + traverseTableAggFld :: (Applicative f) => (a -> f b) -> TableAggFldG a -> f (TableAggFldG b) @@ -262,7 +309,8 @@ type TableAggFlds = TableAggFldsG S.SQLExp data ArgumentExp a = AETableRow !(Maybe Iden) -- ^ table row accessor | AEInput !a - deriving (Show, Eq, Functor, Foldable, Traversable) + deriving (Show, Eq, Functor, Foldable, Traversable, Generic) +instance (Hashable v) => Hashable (ArgumentExp v) type FunctionArgsExpTableRow v = FunctionArgsExpG (ArgumentExp v) @@ -271,8 +319,10 @@ data SelectFromG v | FromIden !Iden | FromFunction !QualifiedFunction !(FunctionArgsExpTableRow v) + -- a definition list !(Maybe [(PGCol, PGScalarType)]) - deriving (Show, Eq, Functor, Foldable, Traversable) + deriving (Show, Eq, Functor, Foldable, Traversable, Generic) +instance (Hashable v) => Hashable (SelectFromG v) type SelectFrom = SelectFromG S.SQLExp @@ -280,7 +330,8 @@ data TablePermG v = TablePerm { _tpFilter :: !(AnnBoolExp v) , _tpLimit :: !(Maybe Int) - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) +instance (Hashable v) => Hashable (TablePermG v) traverseTablePerm :: (Applicative f) @@ -341,19 +392,29 @@ type AnnSimpleSel = AnnSimpleSelG S.SQLExp type AnnAggSelG v = AnnSelG (TableAggFldsG v) v type AnnAggSel = AnnAggSelG S.SQLExp +type ConnectionSelect v = AnnSelG (ConnectionFields v) v +traverseConnectionSelect + :: (Applicative f) + => (a -> f b) + -> ConnectionSelect a -> f (ConnectionSelect b) +traverseConnectionSelect f = + traverseAnnSel (traverse (traverse (traverseConnectionField f))) f + data FunctionArgsExpG a = FunctionArgsExp { _faePositional :: ![a] , _faeNamed :: !(HM.HashMap Text a) - } deriving (Show, Eq, Functor, Foldable, Traversable) + } deriving (Show, Eq, Functor, Foldable, Traversable, Generic) +instance (Hashable a) => Hashable (FunctionArgsExpG a) emptyFunctionArgsExp :: FunctionArgsExpG a emptyFunctionArgsExp = FunctionArgsExp [] HM.empty type FunctionArgExp = FunctionArgsExpG S.SQLExp --- | If argument positional index is less than or equal to length of 'positional' arguments then --- insert the value in 'positional' arguments else insert the value with argument name in 'named' arguments +-- | If argument positional index is less than or equal to length of +-- 'positional' arguments then insert the value in 'positional' arguments else +-- insert the value with argument name in 'named' arguments insertFunctionArg :: FunctionArgName -> Int diff --git a/server/src-lib/Hasura/RQL/Types/BoolExp.hs b/server/src-lib/Hasura/RQL/Types/BoolExp.hs index d37cd857e9d51..56b109bf1f278 100644 --- a/server/src-lib/Hasura/RQL/Types/BoolExp.hs +++ b/server/src-lib/Hasura/RQL/Types/BoolExp.hs @@ -63,6 +63,7 @@ data GExists a instance (NFData a) => NFData (GExists a) instance (Data a) => Plated (GExists a) instance (Cacheable a) => Cacheable (GExists a) +instance (Hashable a) => Hashable (GExists a) gExistsToJSON :: (a -> (Text, Value)) -> GExists a -> Value gExistsToJSON f (GExists qt wh) = @@ -89,6 +90,7 @@ data GBoolExp a instance (NFData a) => NFData (GBoolExp a) instance (Data a) => Plated (GBoolExp a) instance (Cacheable a) => Cacheable (GBoolExp a) +instance (Hashable a) => Hashable (GBoolExp a) gBoolExpTrue :: GBoolExp a gBoolExpTrue = BoolAnd [] @@ -140,6 +142,7 @@ data DWithinGeomOp a = } deriving (Show, Eq, Functor, Foldable, Traversable, Generic, Data) instance (NFData a) => NFData (DWithinGeomOp a) instance (Cacheable a) => Cacheable (DWithinGeomOp a) +instance (Hashable a) => Hashable (DWithinGeomOp a) $(deriveJSON (aesonDrop 6 snakeCase) ''DWithinGeomOp) data DWithinGeogOp a = @@ -150,6 +153,7 @@ data DWithinGeogOp a = } deriving (Show, Eq, Functor, Foldable, Traversable, Generic, Data) instance (NFData a) => NFData (DWithinGeogOp a) instance (Cacheable a) => Cacheable (DWithinGeogOp a) +instance (Hashable a) => Hashable (DWithinGeogOp a) $(deriveJSON (aesonDrop 6 snakeCase) ''DWithinGeogOp) data STIntersectsNbandGeommin a = @@ -159,6 +163,7 @@ data STIntersectsNbandGeommin a = } deriving (Show, Eq, Functor, Foldable, Traversable, Generic, Data) instance (NFData a) => NFData (STIntersectsNbandGeommin a) instance (Cacheable a) => Cacheable (STIntersectsNbandGeommin a) +instance (Hashable a) => Hashable (STIntersectsNbandGeommin a) $(deriveJSON (aesonDrop 4 snakeCase) ''STIntersectsNbandGeommin) data STIntersectsGeomminNband a = @@ -168,6 +173,7 @@ data STIntersectsGeomminNband a = } deriving (Show, Eq, Functor, Foldable, Traversable, Generic, Data) instance (NFData a) => NFData (STIntersectsGeomminNband a) instance (Cacheable a) => Cacheable (STIntersectsGeomminNband a) +instance (Hashable a) => Hashable (STIntersectsGeomminNband a) $(deriveJSON (aesonDrop 4 snakeCase) ''STIntersectsGeomminNband) type CastExp a = M.HashMap PGScalarType [OpExpG a] @@ -227,6 +233,7 @@ data OpExpG a deriving (Eq, Show, Functor, Foldable, Traversable, Generic, Data) instance (NFData a) => NFData (OpExpG a) instance (Cacheable a) => Cacheable (OpExpG a) +instance (Hashable a) => Hashable (OpExpG a) opExpDepCol :: OpExpG a -> Maybe PGCol opExpDepCol = \case @@ -300,6 +307,7 @@ data AnnBoolExpFld a deriving (Show, Eq, Functor, Foldable, Traversable, Generic) instance (NFData a) => NFData (AnnBoolExpFld a) instance (Cacheable a) => Cacheable (AnnBoolExpFld a) +instance (Hashable a) => Hashable (AnnBoolExpFld a) type AnnBoolExp a = GBoolExp (AnnBoolExpFld a) diff --git a/server/src-lib/Hasura/RQL/Types/Column.hs b/server/src-lib/Hasura/RQL/Types/Column.hs index 7c4e2ff81e588..16e36f8c417f0 100644 --- a/server/src-lib/Hasura/RQL/Types/Column.hs +++ b/server/src-lib/Hasura/RQL/Types/Column.hs @@ -167,6 +167,7 @@ data PGColumnInfo } deriving (Show, Eq, Generic) instance NFData PGColumnInfo instance Cacheable PGColumnInfo +instance Hashable PGColumnInfo $(deriveToJSON (aesonDrop 3 snakeCase) ''PGColumnInfo) onlyIntCols :: [PGColumnInfo] -> [PGColumnInfo] diff --git a/server/src-lib/Hasura/RQL/Types/Common.hs b/server/src-lib/Hasura/RQL/Types/Common.hs index fccf29a13a298..27cf5f06da93c 100644 --- a/server/src-lib/Hasura/RQL/Types/Common.hs +++ b/server/src-lib/Hasura/RQL/Types/Common.hs @@ -142,6 +142,7 @@ data RelInfo } deriving (Show, Eq, Generic) instance NFData RelInfo instance Cacheable RelInfo +instance Hashable RelInfo $(deriveToJSON (aesonDrop 2 snakeCase) ''RelInfo) newtype FieldName diff --git a/server/src-lib/Hasura/RQL/Types/DML.hs b/server/src-lib/Hasura/RQL/Types/DML.hs index 7851770f2fb61..e19e54369880f 100644 --- a/server/src-lib/Hasura/RQL/Types/DML.hs +++ b/server/src-lib/Hasura/RQL/Types/DML.hs @@ -101,6 +101,7 @@ instance (FromJSON a) => FromJSON (DMLQuery a) where newtype OrderType = OrderType { unOrderType :: S.OrderType } deriving (Show, Eq, Lift, Generic) +instance Hashable OrderType instance FromJSON OrderType where parseJSON = @@ -112,6 +113,7 @@ instance FromJSON OrderType where newtype NullsOrder = NullsOrder { unNullsOrder :: S.NullsOrder } deriving (Show, Eq, Lift, Generic) +instance Hashable NullsOrder instance FromJSON NullsOrder where parseJSON = @@ -176,7 +178,8 @@ data OrderByItemG a { obiType :: !(Maybe OrderType) , obiColumn :: !a , obiNulls :: !(Maybe NullsOrder) - } deriving (Show, Eq, Lift, Functor, Foldable, Traversable) + } deriving (Show, Eq, Lift, Functor, Foldable, Traversable, Generic) +instance (Hashable a) => Hashable (OrderByItemG a) type OrderByItem = OrderByItemG OrderByCol diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs index 3449501f77413..8e1b3a67117f9 100644 --- a/server/src-lib/Hasura/SQL/DML.hs +++ b/server/src-lib/Hasura/SQL/DML.hs @@ -35,6 +35,7 @@ data Select } deriving (Show, Eq, Generic, Data) instance NFData Select instance Cacheable Select +instance Hashable Select mkSelect :: Select mkSelect = Select Nothing [] Nothing @@ -43,7 +44,7 @@ mkSelect = Select Nothing [] Nothing newtype LimitExp = LimitExp SQLExp - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL LimitExp where toSQL (LimitExp se) = @@ -51,7 +52,7 @@ instance ToSQL LimitExp where newtype OffsetExp = OffsetExp SQLExp - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL OffsetExp where toSQL (OffsetExp se) = @@ -59,7 +60,7 @@ instance ToSQL OffsetExp where newtype OrderByExp = OrderByExp [OrderByItem] - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) data OrderByItem = OrderByItem @@ -69,6 +70,7 @@ data OrderByItem } deriving (Show, Eq, Generic, Data) instance NFData OrderByItem instance Cacheable OrderByItem +instance Hashable OrderByItem instance ToSQL OrderByItem where toSQL (OrderByItem e ot no) = @@ -78,6 +80,7 @@ data OrderType = OTAsc | OTDesc deriving (Show, Eq, Lift, Generic, Data) instance NFData OrderType instance Cacheable OrderType +instance Hashable OrderType instance ToSQL OrderType where toSQL OTAsc = "ASC" @@ -89,6 +92,7 @@ data NullsOrder deriving (Show, Eq, Lift, Generic, Data) instance NFData NullsOrder instance Cacheable NullsOrder +instance Hashable NullsOrder instance ToSQL NullsOrder where toSQL NFirst = "NULLS FIRST" @@ -100,7 +104,7 @@ instance ToSQL OrderByExp where newtype GroupByExp = GroupByExp [SQLExp] - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL GroupByExp where toSQL (GroupByExp idens) = @@ -108,7 +112,7 @@ instance ToSQL GroupByExp where newtype FromExp = FromExp [FromItem] - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL FromExp where toSQL (FromExp items) = @@ -148,7 +152,7 @@ mkRowExp extrs = let newtype HavingExp = HavingExp BoolExp - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL HavingExp where toSQL (HavingExp be) = @@ -156,7 +160,7 @@ instance ToSQL HavingExp where newtype WhereFrag = WhereFrag { getWFBoolExp :: BoolExp } - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL WhereFrag where toSQL (WhereFrag be) = @@ -188,6 +192,7 @@ data Qual deriving (Show, Eq, Generic, Data) instance NFData Qual instance Cacheable Qual +instance Hashable Qual mkQual :: QualifiedTable -> Qual mkQual = QualTable @@ -205,6 +210,7 @@ data QIden deriving (Show, Eq, Generic, Data) instance NFData QIden instance Cacheable QIden +instance Hashable QIden instance ToSQL QIden where toSQL (QIden qual iden) = @@ -212,7 +218,7 @@ instance ToSQL QIden where newtype SQLOp = SQLOp {sqlOpTxt :: T.Text} - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) incOp :: SQLOp incOp = SQLOp "+" @@ -234,7 +240,7 @@ jsonbDeleteAtPathOp = SQLOp "#-" newtype TypeAnn = TypeAnn { unTypeAnn :: T.Text } - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL TypeAnn where toSQL (TypeAnn ty) = "::" <> TB.text ty @@ -264,6 +270,7 @@ data CountType deriving (Show, Eq, Generic, Data) instance NFData CountType instance Cacheable CountType +instance Hashable CountType instance ToSQL CountType where toSQL CTStar = "*" @@ -274,7 +281,7 @@ instance ToSQL CountType where newtype TupleExp = TupleExp [SQLExp] - deriving (Show, Eq, NFData, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance ToSQL TupleExp where toSQL (TupleExp exps) = @@ -306,6 +313,7 @@ data SQLExp deriving (Show, Eq, Generic, Data) instance NFData SQLExp instance Cacheable SQLExp +instance Hashable SQLExp withTyAnn :: PGScalarType -> SQLExp -> SQLExp withTyAnn colTy v = SETyAnn v . mkTypeAnn $ PGTypeScalar colTy @@ -315,7 +323,7 @@ instance J.ToJSON SQLExp where newtype Alias = Alias { getAlias :: Iden } - deriving (Show, Eq, NFData, Hashable, Data, Cacheable) + deriving (Show, Eq, NFData, Data, Cacheable, Hashable) instance IsIden Alias where toIden (Alias iden) = iden @@ -381,6 +389,7 @@ data Extractor = Extractor !SQLExp !(Maybe Alias) deriving (Show, Eq, Generic, Data) instance NFData Extractor instance Cacheable Extractor +instance Hashable Extractor mkSQLOpExp :: SQLOp @@ -428,6 +437,7 @@ data DistinctExpr deriving (Show, Eq, Generic, Data) instance NFData DistinctExpr instance Cacheable DistinctExpr +instance Hashable DistinctExpr instance ToSQL DistinctExpr where toSQL DistinctSimple = "DISTINCT" @@ -441,6 +451,7 @@ data FunctionArgs } deriving (Show, Eq, Generic, Data) instance NFData FunctionArgs instance Cacheable FunctionArgs +instance Hashable FunctionArgs instance ToSQL FunctionArgs where toSQL (FunctionArgs positionalArgs namedArgsMap) = @@ -455,6 +466,7 @@ data DefinitionListItem } deriving (Show, Eq, Data, Generic) instance NFData DefinitionListItem instance Cacheable DefinitionListItem +instance Hashable DefinitionListItem instance ToSQL DefinitionListItem where toSQL (DefinitionListItem column columnType) = @@ -467,6 +479,7 @@ data FunctionAlias } deriving (Show, Eq, Data, Generic) instance NFData FunctionAlias instance Cacheable FunctionAlias +instance Hashable FunctionAlias mkSimpleFunctionAlias :: Iden -> FunctionAlias mkSimpleFunctionAlias identifier = @@ -491,6 +504,7 @@ data FunctionExp } deriving (Show, Eq, Generic, Data) instance NFData FunctionExp instance Cacheable FunctionExp +instance Hashable FunctionExp instance ToSQL FunctionExp where toSQL (FunctionExp qf args alsM) = @@ -507,6 +521,7 @@ data FromItem deriving (Show, Eq, Generic, Data) instance NFData FromItem instance Cacheable FromItem +instance Hashable FromItem mkSelFromItem :: Select -> Alias -> FromItem mkSelFromItem = FISelect (Lateral False) @@ -536,7 +551,7 @@ instance ToSQL FromItem where toSQL je newtype Lateral = Lateral Bool - deriving (Show, Eq, Data, NFData, Cacheable) + deriving (Show, Eq, Data, NFData, Cacheable, Hashable) instance ToSQL Lateral where toSQL (Lateral True) = "LATERAL" @@ -551,6 +566,7 @@ data JoinExpr } deriving (Show, Eq, Generic, Data) instance NFData JoinExpr instance Cacheable JoinExpr +instance Hashable JoinExpr instance ToSQL JoinExpr where toSQL je = @@ -567,6 +583,7 @@ data JoinType deriving (Eq, Show, Generic, Data) instance NFData JoinType instance Cacheable JoinType +instance Hashable JoinType instance ToSQL JoinType where toSQL Inner = "INNER JOIN" @@ -580,6 +597,7 @@ data JoinCond deriving (Show, Eq, Generic, Data) instance NFData JoinCond instance Cacheable JoinCond +instance Hashable JoinCond instance ToSQL JoinCond where toSQL (JoinOn be) = @@ -603,6 +621,7 @@ data BoolExp deriving (Show, Eq, Generic, Data) instance NFData BoolExp instance Cacheable BoolExp +instance Hashable BoolExp -- removes extraneous 'AND true's simplifyBoolExp :: BoolExp -> BoolExp @@ -658,6 +677,7 @@ data BinOp = AndOp | OrOp deriving (Show, Eq, Generic, Data) instance NFData BinOp instance Cacheable BinOp +instance Hashable BinOp instance ToSQL BinOp where toSQL AndOp = "AND" @@ -686,6 +706,7 @@ data CompareOp deriving (Eq, Generic, Data) instance NFData CompareOp instance Cacheable CompareOp +instance Hashable CompareOp instance Show CompareOp where show = \case @@ -832,7 +853,7 @@ instance ToSQL SQLConflict where newtype ValuesExp = ValuesExp [TupleExp] - deriving (Show, Eq, Data, NFData, Cacheable) + deriving (Show, Eq, Data, NFData, Cacheable, Hashable) instance ToSQL ValuesExp where toSQL (ValuesExp tuples) = diff --git a/server/src-lib/Hasura/SQL/Types.hs b/server/src-lib/Hasura/SQL/Types.hs index 3769a5f6f052f..cd7a634763dd2 100644 --- a/server/src-lib/Hasura/SQL/Types.hs +++ b/server/src-lib/Hasura/SQL/Types.hs @@ -303,7 +303,7 @@ type QualifiedFunction = QualifiedObject FunctionName newtype PGDescription = PGDescription { getPGDescription :: T.Text } - deriving (Show, Eq, FromJSON, ToJSON, Q.FromCol, NFData, Cacheable) + deriving (Show, Eq, FromJSON, ToJSON, Q.FromCol, NFData, Cacheable, Hashable) newtype PGCol = PGCol { getPGColTxt :: T.Text } From a16bc0bfd94c5bd0a52cc0ae80b9a5735396ffa5 Mon Sep 17 00:00:00 2001 From: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Date: Wed, 29 Apr 2020 18:29:33 +0530 Subject: [PATCH 03/29] implement 'mkConnectionSelect' function with minimal cursors. --- server/src-lib/Hasura/GraphQL/Resolve.hs | 14 +- .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 10 +- .../src-lib/Hasura/GraphQL/Resolve/Types.hs | 4 +- server/src-lib/Hasura/GraphQL/Schema.hs | 5 +- server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs | 1 - server/src-lib/Hasura/RQL/DML/Select.hs | 3 +- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 157 +++++++++++------- server/src-lib/Hasura/RQL/DML/Select/Types.hs | 4 +- server/src-lib/Hasura/RQL/Types/Common.hs | 4 +- 9 files changed, 120 insertions(+), 82 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve.hs b/server/src-lib/Hasura/GraphQL/Resolve.hs index 3e4b03357d4e9..7f3404f63a569 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve.hs @@ -44,7 +44,7 @@ data QueryRootFldAST v = QRFPk !(DS.AnnSimpleSelG v) | QRFSimple !(DS.AnnSimpleSelG v) | QRFAgg !(DS.AnnAggSelG v) - | QRFConnection !(DS.ConnectionSelect v) + | QRFConnection !(Maybe (NonEmpty PGCol)) !(DS.ConnectionSelect v) | QRFActionSelect !(DS.AnnSimpleSelG v) deriving (Show, Eq) @@ -60,7 +60,7 @@ traverseQueryRootFldAST f = \case QRFPk s -> QRFPk <$> DS.traverseAnnSimpleSel f s QRFSimple s -> QRFSimple <$> DS.traverseAnnSimpleSel f s QRFAgg s -> QRFAgg <$> DS.traverseAnnAggSel f s - QRFConnection s -> QRFConnection <$> DS.traverseConnectionSelect f s + QRFConnection pkey s -> QRFConnection pkey <$> DS.traverseConnectionSelect f s QRFActionSelect s -> QRFActionSelect <$> DS.traverseAnnSimpleSel f s toPGQuery :: QueryRootFldResolved -> Q.Query @@ -69,7 +69,7 @@ toPGQuery = \case QRFSimple s -> DS.selectQuerySQL DS.JASMultipleRows s QRFAgg s -> DS.selectAggQuerySQL s QRFActionSelect s -> DS.selectQuerySQL DS.JASSingleObject s - QRFConnection s -> Q.fromBuilder $ toSQL $ DS.mkConnectionSelect s + QRFConnection pkey s -> Q.fromBuilder $ toSQL $ DS.mkConnectionSelect pkey s validateHdrs :: (Foldable t, QErrM m) => UserInfo -> t Text -> m () @@ -107,10 +107,10 @@ queryFldToPGAST fld = do QRFAgg <$> RS.convertFuncQueryAgg ctx fld QCActionFetch ctx -> QRFActionSelect <$> RA.resolveAsyncActionQuery userInfo ctx fld - QCSelectConnection ctx -> do - QRFConnection <$> RS.convertConnectionSelect ctx fld - QCFuncConnection ctx -> do - QRFConnection <$> RS.convertConnectionFuncQuery ctx fld + QCSelectConnection pk ctx -> + QRFConnection pk <$> RS.convertConnectionSelect ctx fld + QCFuncConnection pk ctx -> + QRFConnection pk <$> RS.convertConnectionFuncQuery ctx fld mutFldToTx :: ( HasVersion diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index 5b9aaca18e0dc..c31072f9e3d9c 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -170,14 +170,14 @@ parsePageInfoSelectionSet parsePageInfoSelectionSet fldTy selSet = fmap toFields $ withSelSet selSet $ \f -> case _fName f of - "__typename" -> pure $ RS.PageInfoTypename $ G.unName $ G.unNamedType fldTy - "hasNextPage" -> pure $ RS.PageInfoHasNextPage + "__typename" -> pure $ RS.PageInfoTypename $ G.unName $ G.unNamedType fldTy + "hasNextPage" -> pure $ RS.PageInfoHasNextPage "hasPreviousPage" -> pure $ RS.PageInfoHasPreviousPage - "startCursor" -> pure $ RS.PageInfoStartCursor - "endCursor" -> pure $ RS.PageInfoEndCursor + "startCursor" -> pure $ RS.PageInfoStartCursor + "endCursor" -> pure $ RS.PageInfoEndCursor -- "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet -- "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet - G.Name t -> throw500 $ "unexpected field in PageInfo node: " <> t + G.Name t -> throw500 $ "unexpected field in PageInfo node: " <> t type TableArgs = RS.TableArgsG UnresolvedVal diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs index 3dda96769d2ed..297778a9a0e62 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs @@ -29,12 +29,12 @@ import qualified Hasura.SQL.DML as S data QueryCtx = QCSelect !SelOpCtx - | QCSelectConnection !SelOpCtx + | QCSelectConnection !(Maybe (NonEmpty PGCol)) !SelOpCtx | QCSelectPkey !SelPkOpCtx | QCSelectAgg !SelOpCtx | QCFuncQuery !FuncQOpCtx | QCFuncAggQuery !FuncQOpCtx - | QCFuncConnection !FuncQOpCtx + | QCFuncConnection !(Maybe (NonEmpty PGCol)) !FuncQOpCtx | QCActionFetch !ActionSelectOpContext deriving (Show, Eq) diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index 0126f01838fb6..af1ac0082fd29 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -363,6 +363,7 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM ] } where + primaryKeyColumn = fmap (fmap pgiColumn . _pkColumns) primaryKey makeFieldMap = mapFromL (_fiName . snd) customRootFields = _tcCustomRootFields tableConfig colGNameMap = mkPGColGNameMap $ getCols fields @@ -421,7 +422,7 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM getSelDet (selFltr, pLimit, hdrs, _) = selFldHelper QCSelect (mkSelFld selCustName) selFltr pLimit hdrs getSelConnectionDet (selFltr, pLimit, hdrs, _) = - selFldHelper QCSelectConnection (mkSelFldConnection Nothing) selFltr pLimit hdrs + selFldHelper (QCSelectConnection primaryKeyColumn) (mkSelFldConnection Nothing) selFltr pLimit hdrs selAggCustName = getCustomNameWith _tcrfSelectAggregate getSelAggDet (Just (selFltr, pLimit, hdrs, True)) = @@ -445,7 +446,7 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM funcFldHelper QCFuncQuery mkFuncQueryFld selFltr pLimit hdrs getFuncQueryConnectionFlds (selFltr, pLimit, hdrs, _) = - funcFldHelper QCFuncConnection mkFuncQueryConnectionFld selFltr pLimit hdrs + funcFldHelper (QCFuncConnection primaryKeyColumn) mkFuncQueryConnectionFld selFltr pLimit hdrs getFuncAggQueryFlds (selFltr, pLimit, hdrs, True) = funcFldHelper QCFuncAggQuery mkFuncAggQueryFld selFltr pLimit hdrs diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs index 25329e7417535..ee37fafc15cde 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs @@ -258,7 +258,6 @@ buildSchemaCacheRule = proc (catalogMetadata, invalidationKeys) -> do -- custom types resolvedCustomTypes <- bindA -< resolveCustomTypes tableCache customTypes - -- actions actionCache <- (mapFromL _amName actions >- returnA) >-> (| Inc.keyed (\_ action -> do let ActionMetadata name comment def actionPermissions = action diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index 9a21d761919c5..ab8a892b435b2 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -265,8 +265,7 @@ convSelectQuery sessVarBldr prepArgBuilder (DMLQuery qt selQ) = do let fieldInfo = _tciFieldInfoMap $ _tiCoreInfo tabInfo extSelQ <- resolveStar fieldInfo selPermInfo selQ validateHeaders $ spiRequiredHeaders selPermInfo - convSelectQ fieldInfo selPermInfo - extSelQ sessVarBldr prepArgBuilder + convSelectQ fieldInfo selPermInfo extSelQ sessVarBldr prepArgBuilder selectP2 :: JsonAggSelect -> (AnnSimpleSel, DS.Seq Q.PrepArg) -> Q.TxE QErr EncJSON selectP2 jsonAggSelect (sel, p) = diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index 360c5a4245108..cc8a1cca362de 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -3,11 +3,13 @@ module Hasura.RQL.DML.Select.Internal , mkAggSelect -- , mkConnectionSelect -- , JoinCandidate(..) + , mkConnectionSelect , module Hasura.RQL.DML.Select.Types ) where import Control.Lens hiding (op) +import Control.Monad.Writer.Strict import Data.List (delete, sort) import Instances.TH.Lift () @@ -364,7 +366,7 @@ processAnnOrderByCol pfx parAls arrRelCtx strfyNum = \case (extr, arrFlds) = mkAggObExtrAndFlds annAggOb selFld = TAFAgg arrFlds bn = mkBaseNode False (Prefixes arrPfx pfx) fldName selFld tabFrom - tabPerm noTableArgs strfyNum + tabPerm noTableArgs Nothing strfyNum aggNode = ArrNode [extr] colMapping $ mergeBaseNodes bn $ mkEmptyBaseNode arrPfx tabFrom obAls = arrPfx <> Iden "." <> toIden fldName @@ -415,7 +417,7 @@ aggSelToArrNode pfxs als aggSel = mergedBN = foldr mergeBaseNodes emptyBN allBNs mkAggBaseNode (fn, selFld) = - mkBaseNode subQueryReq pfxs fn selFld tabFrm tabPerm tabArgs strfyNum + mkBaseNode subQueryReq pfxs fn selFld tabFrm tabPerm tabArgs Nothing strfyNum selFldToExtr (FieldName t, fld) = (:) (S.SELit t) $ pure $ case fld of TAFAgg flds -> aggFldToExp flds @@ -428,39 +430,6 @@ aggSelToArrNode pfxs als aggSel = subQueryReq = hasAggFld aggFlds --- connectionSelToArrNode --- :: Prefixes -> FieldName -> ArrRelConnection S.SQLExp -> ArrNode --- connectionSelToArrNode pfxs als aggSel = --- ArrNode [extr] colMapping mergedBN --- where --- AnnRelG _ colMapping annSel = aggSel --- AnnSelG aggFlds tabFrm tabPerm tabArgs strfyNum = annSel --- fldAls = S.Alias $ toIden als - --- extr = flip S.Extractor (Just fldAls) $ S.applyJsonBuildObj $ --- concatMap selFldToExtr aggFlds - --- permLimit = _tpLimit tabPerm --- ordBy = _bnOrderBy mergedBN - --- allBNs = map mkAggBaseNode aggFlds --- emptyBN = mkEmptyBaseNode (_pfThis pfxs) tabFrm --- mergedBN = foldr mergeBaseNodes emptyBN allBNs - --- mkAggBaseNode (fn, selFld) = --- mkBaseNode subQueryReq pfxs fn selFld tabFrm tabPerm tabArgs strfyNum - --- selFldToExtr (FieldName t, fld) = (:) (S.SELit t) $ pure $ case fld of --- TAFAgg flds -> aggFldToExp flds --- TAFNodes _ -> --- withJsonAggExtr subQueryReq permLimit ordBy $ S.Alias $ Iden t --- TAFExp e -> --- -- bool_or to force aggregation --- S.SEFnApp "coalesce" --- [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing - --- subQueryReq = hasAggFld aggFlds - hasAggFld :: Foldable t => t (a, TableAggFldG v) -> Bool hasAggFld = any (isTabAggFld . snd) where @@ -557,6 +526,17 @@ mkOrdByItems pfx fldAls orderByM strfyNum arrRelCtx = getOrdByAggNode (OBNArrNode als node) = Just (als, node) getOrdByAggNode _ = Nothing +cursorAlias :: S.Alias +cursorAlias = S.Alias $ Iden "__cursor" + +mkCursorExtractor :: Iden -> [PGCol] -> S.SQLExp +mkCursorExtractor pfx columns = + flip S.SETyAnn S.textTypeAnn $ + S.applyJsonBuildObj $ flip concatMap columns $ + \column -> [ S.SELit $ getPGColTxt column + , S.mkQIdenExp (mkBaseTableAls pfx) column + ] + mkBaseNode :: Bool -> Prefixes @@ -565,10 +545,11 @@ mkBaseNode -> SelectFrom -> TablePerm -> TableArgs + -> Maybe (NonEmpty PGCol) -> Bool -> BaseNode mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom - tablePerm tableArgs strfyNum = + tablePerm tableArgs primaryKeyColumns strfyNum = BaseNode thisPfx distExprM fromItem finalWhere ordByExpM finalLimit offsetM allExtrs allObjsWithOb allArrsWithOb computedFields @@ -599,6 +580,8 @@ mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom let arrFlds = mapMaybe getAnnArr flds arrRelCtx = mkArrRelCtx arrFlds selExtr = buildJsonObject thisPfx fldAls arrRelCtx strfyNum flds + cursorExtrs = maybe [] (pure . (cursorAlias,) . mkCursorExtractor thisPfx . toList) + primaryKeyColumns -- all object relationships objNodes = HM.fromListWith mergeObjNodes $ map mkObjItem (mapMaybe getAnnObj flds) @@ -614,7 +597,7 @@ mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom allObjs = HM.unionWith mergeObjNodes objNodes ordByObjs allArrs = HM.unionWith mergeArrNodes arrNodes ordByArrs - in ( HM.fromList $ selExtr:obExtrs <> distExtrs + in ( HM.fromList $ selExtr:obExtrs <> distExtrs <> cursorExtrs , allObjs , allArrs , computedFieldNodes @@ -683,7 +666,7 @@ mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom -- process a computed field, which returns a table mkComputedFieldTable (fld, jsonAggSelect, sel) = let prefixes = Prefixes (mkComputedFieldTableAls thisPfx fld) thisPfx - baseNode = annSelToBaseNode False prefixes fld sel + baseNode = annSelToBaseNode False prefixes fld Nothing sel in (fld, CFTableNode jsonAggSelect baseNode) getAnnObj (f, annFld) = case annFld of @@ -698,27 +681,28 @@ mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom FComputedField (CFSTable jas sel) -> Just (f, jas, sel) _ -> Nothing -annSelToBaseNode :: Bool -> Prefixes -> FieldName -> AnnSimpleSel -> BaseNode -annSelToBaseNode subQueryReq pfxs fldAls annSel = - mkBaseNode subQueryReq pfxs fldAls (TAFNodes selFlds) tabFrm tabPerm tabArgs strfyNum +annSelToBaseNode + :: Bool -> Prefixes -> FieldName -> Maybe (NonEmpty PGCol) -> AnnSimpleSel -> BaseNode +annSelToBaseNode subQueryReq pfxs fldAls pkeyCols annSel = + mkBaseNode subQueryReq pfxs fldAls (TAFNodes selFlds) tabFrm tabPerm tabArgs pkeyCols strfyNum where AnnSelG selFlds tabFrm tabPerm tabArgs strfyNum = annSel mkObjNode :: Prefixes -> (FieldName, ObjSel) -> ObjNode mkObjNode pfxs (fldName, AnnRelG _ rMapn rAnnSel) = - ObjNode rMapn $ annSelToBaseNode False pfxs fldName rAnnSel + ObjNode rMapn $ annSelToBaseNode False pfxs fldName Nothing rAnnSel mkArrNode :: Bool -> Prefixes -> (FieldName, ArrSel) -> ArrNode mkArrNode subQueryReq pfxs (fldName, annArrSel) = case annArrSel of ASSimple annArrRel -> - let bn = annSelToBaseNode subQueryReq pfxs fldName $ aarAnnSel annArrRel + let bn = annSelToBaseNode subQueryReq pfxs fldName Nothing $ aarAnnSel annArrRel permLimit = getPermLimit $ aarAnnSel annArrRel extr = asJsonAggExtr JASMultipleRows (S.toAlias fldName) subQueryReq permLimit $ _bnOrderBy bn in ArrNode [extr] (aarMapping annArrRel) bn ASAgg annAggSel -> aggSelToArrNode pfxs fldName annAggSel - -- TODO: ASConnection connectionSel -> connectionSelToArrNode pfxs fldName connectionSel + ASConnection connectionSel -> connectionSelToArrNode pfxs fldName connectionSel injectJoinCond :: S.BoolExp -- ^ Join condition -> S.BoolExp -- ^ Where condition @@ -791,23 +775,12 @@ baseNodeToSel joinCond baseNode = } in S.mkLateralFromItem sel als --- mkConnectionSelect :: ConnectionSelect S.SQLExp -> S.Select --- mkConnectionSelect annConnectionSel = --- prefixNumToAliases $ arrNodeToSelect bn extr $ S.BELit True --- where --- connectionSelect = AnnRelG rootRelName HM.empty annConnectionSel --- rootIden = Iden "root" --- rootPrefix = Prefixes rootIden rootIden --- ArrNode extr _ bn = --- connectionSelToArrNode rootPrefix (FieldName "root") connectionSelect - data TableSource = TableSource { _tsFrom :: !SelectFrom , _tsPermission :: !TablePerm , _tsArgs :: !TableArgs - } - deriving (Show, Eq, Generic) + } deriving (Show, Eq, Generic) instance Hashable TableSource @@ -825,8 +798,8 @@ instance Hashable TableSource data BuildState = BuildState - { _bsAliasGenerator :: !Int - , _bsJoinTree :: !JoinTree + { _bsAliasGenerator :: !Int + , _bsJoinTree :: !JoinTree } type Build = State BuildState @@ -923,6 +896,72 @@ mkSQLSelect jsonAggSelect annSel = $ _bnOrderBy baseNode rootFldIden = toIden rootFldName rootPrefix = Prefixes rootFldIden rootFldIden - baseNode = annSelToBaseNode False rootPrefix rootFldName annSel + baseNode = annSelToBaseNode False rootPrefix rootFldName Nothing annSel rootFldName = FieldName "root" rootFldAls = S.Alias $ toIden rootFldName + +mkConnectionSelectNodeAndExtr + :: Prefixes + -> FieldName + -> Maybe (NonEmpty PGCol) + -> ConnectionSelect S.SQLExp + -> (BaseNode, S.Extractor) +mkConnectionSelectNodeAndExtr pfxs fieldAlias primaryKeyColumns connectionSelect = + (baseNode, extractor) + where + AnnSelG fields selFrom perm args strfyNum = connectionSelect + fieldIden = toIden fieldAlias + mkSimpleJsonAgg rowExp ob = + let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob + in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing + + baseNode = foldr mergeBaseNodes (mkEmptyBaseNode fieldIden selFrom) nodeList + encodeBase64 t = S.SEFnApp "encode" [S.SETyAnn t $ S.TypeAnn "bytea", S.SELit "base64"] Nothing + + extractor = S.Extractor extractorExp $ Just $ S.Alias fieldIden + (extractorExp, nodeList) = runWriter $ + fmap (S.applyJsonBuildObj . concat) $ forM fields $ + \(FieldName fieldText, field) -> (S.SELit fieldText:) . pure <$> + case field of + ConnectionTypename t -> pure $ S.SELit t + ConnectionPageInfo f -> pure $ processPageInfoFields f + ConnectionEdges edges -> + -- TODO:- Add order by here + fmap (flip mkSimpleJsonAgg Nothing . S.applyJsonBuildObj . concat) $ forM edges $ + \(FieldName edgeText, edge) -> (S.SELit edgeText:) . pure <$> + case edge of + EdgeTypename t -> pure $ S.SELit t + EdgeCursor -> pure $ encodeBase64 $ S.SEIden (toIden cursorAlias) + EdgeNode annFields -> do + let rootFieldName = FieldName $ "root." <> fieldText <> "." <> edgeText + rootFieldIden = toIden rootFieldName + edgeBaseNode = annSelToBaseNode False pfxs rootFieldName primaryKeyColumns $ + AnnSelG annFields selFrom perm args strfyNum + tell [edgeBaseNode] + pure $ S.SEIden rootFieldIden + + processPageInfoFields infoFields = + S.applyJsonBuildObj $ flip concatMap infoFields $ + \(FieldName fieldText, field) -> S.SELit fieldText: case field of + PageInfoTypename t -> pure $ S.SELit t + PageInfoHasNextPage -> pure $ S.SEBool $ S.BELit True + PageInfoHasPreviousPage -> pure $ S.SEBool $ S.BELit False + PageInfoStartCursor -> pure $ S.SELit "start cursor" + PageInfoEndCursor -> pure $ S.SELit "end cursor" + +connectionSelToArrNode + :: Prefixes -> FieldName -> ArrRelConnection S.SQLExp -> ArrNode +connectionSelToArrNode pfxs als arrRelConnSel = + -- TODO:- Get primary key context here. + let (baseNode, extr) = mkConnectionSelectNodeAndExtr pfxs als Nothing connSel + in ArrNode [extr] colMapping baseNode + where + AnnRelG _ colMapping connSel = arrRelConnSel + +mkConnectionSelect :: Maybe (NonEmpty PGCol) -> ConnectionSelect S.SQLExp -> S.Select +mkConnectionSelect primaryKeyColumns connectionSelect = + let rootFieldName = FieldName "root" + rootIden = toIden rootFieldName + prefixes = Prefixes rootIden rootIden + (baseNode, extractor) = mkConnectionSelectNodeAndExtr prefixes rootFieldName primaryKeyColumns connectionSelect + in prefixNumToAliases $ arrNodeToSelect baseNode [extractor] $ S.BELit True diff --git a/server/src-lib/Hasura/RQL/DML/Select/Types.hs b/server/src-lib/Hasura/RQL/DML/Select/Types.hs index a078eb0f814e9..759e1d6b8b201 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Types.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Types.hs @@ -274,8 +274,8 @@ traverseEdgeField :: (Applicative f) => (a -> f b) -> EdgeField a -> f (EdgeField b) traverseEdgeField f = \case - EdgeTypename t -> pure $ EdgeTypename t - EdgeCursor -> pure EdgeCursor + EdgeTypename t -> pure $ EdgeTypename t + EdgeCursor -> pure EdgeCursor EdgeNode fields -> EdgeNode <$> traverseAnnFlds f fields data ConnectionField v diff --git a/server/src-lib/Hasura/RQL/Types/Common.hs b/server/src-lib/Hasura/RQL/Types/Common.hs index 27cf5f06da93c..9c70b544826b7 100644 --- a/server/src-lib/Hasura/RQL/Types/Common.hs +++ b/server/src-lib/Hasura/RQL/Types/Common.hs @@ -44,7 +44,7 @@ import Data.Aeson import Data.Aeson.Casing import Data.Aeson.TH import Instances.TH.Lift () -import Language.Haskell.TH.Syntax (Q, TExp, Lift) +import Language.Haskell.TH.Syntax (Lift, Q, TExp) import qualified Data.HashMap.Strict as HM import qualified Data.Text as T @@ -212,7 +212,7 @@ data PrimaryKey a = PrimaryKey { _pkConstraint :: !Constraint , _pkColumns :: !(NonEmpty a) - } deriving (Show, Eq, Generic) + } deriving (Show, Eq, Generic, Foldable) instance (NFData a) => NFData (PrimaryKey a) instance (Cacheable a) => Cacheable (PrimaryKey a) $(makeLenses ''PrimaryKey) From e257138fe9feb8dfd45e096c4514432c7d99ef92 Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi Date: Fri, 8 May 2020 12:22:40 +0530 Subject: [PATCH 04/29] validation support for unions and interfaces --- server/cabal.project.freeze | 10 +- server/graphql-engine.cabal | 4 +- .../Data/HashMap/Strict/InsOrd/Extended.hs | 8 +- server/src-lib/Data/Sequence/NonEmpty.hs | 21 +- server/src-lib/Hasura/GraphQL/NormalForm.hs | 229 +++++++ .../Hasura/GraphQL/Resolve/Introspect.hs | 6 +- server/src-lib/Hasura/GraphQL/Validate.hs | 8 +- .../Hasura/GraphQL/Validate/Context.hs | 19 +- .../src-lib/Hasura/GraphQL/Validate/Field.hs | 350 ----------- .../Hasura/GraphQL/Validate/SelectionSet.hs | 578 ++++++++++++++++++ .../src-lib/Hasura/GraphQL/Validate/Types.hs | 163 +++-- 11 files changed, 926 insertions(+), 470 deletions(-) create mode 100644 server/src-lib/Hasura/GraphQL/NormalForm.hs delete mode 100644 server/src-lib/Hasura/GraphQL/Validate/Field.hs create mode 100644 server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs diff --git a/server/cabal.project.freeze b/server/cabal.project.freeze index ecb9bd7c433f7..b010902930266 100644 --- a/server/cabal.project.freeze +++ b/server/cabal.project.freeze @@ -54,7 +54,7 @@ constraints: any.Cabal ==2.4.0.1, any.bytestring ==0.10.8.2, any.bytestring-builder ==0.10.8.2.0, bytestring-builder +bytestring_has_builder, - any.bytestring-strict-builder ==0.4.5.1, + any.bytestring-strict-builder ==0.4.5.3, any.bytestring-tree-builder ==0.2.7.2, any.cabal-doctest ==1.0.8, any.call-stack ==0.1.0, @@ -106,7 +106,7 @@ constraints: any.Cabal ==2.4.0.1, any.data-serializer ==0.3.4, any.data-textual ==0.3.0.2, any.deepseq ==1.4.4.0, - any.deferred-folds ==0.9.10, + any.deferred-folds ==0.9.10.1, any.dense-linear-algebra ==0.1.0.0, any.dependent-map ==0.2.4.0, any.dependent-sum ==0.4, @@ -165,7 +165,7 @@ constraints: any.Cabal ==2.4.0.1, http2 -devel, any.hvect ==0.4.0.0, any.immortal ==0.2.2.1, - any.insert-ordered-containers ==0.2.1.0, + any.insert-ordered-containers ==0.2.3, any.integer-gmp ==1.0.2.0, any.integer-logarithms ==1.0.3, integer-logarithms -check-bounds +integer-gmp, @@ -257,7 +257,7 @@ constraints: any.Cabal ==2.4.0.1, scientific -bytestring-builder -integer-simple, any.semigroupoids ==5.3.2, semigroupoids +comonad +containers +contravariant +distributive +doctests +tagged +unordered-containers, - any.semigroups ==0.18.5, + any.semigroups ==0.19.1, semigroups +binary +bytestring -bytestring-builder +containers +deepseq +hashable +tagged +text +transformers +unordered-containers, any.semver ==0.3.3.1, any.setenv ==0.1.1.3, @@ -280,7 +280,7 @@ constraints: any.Cabal ==2.4.0.1, any.template-haskell ==2.14.0.0, any.terminal-size ==0.3.2.1, any.text ==1.2.3.1, - any.text-builder ==0.6.5, + any.text-builder ==0.6.6.1, any.text-conversions ==0.3.0, any.text-latin1 ==0.3.1, any.text-printer ==0.5, diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 2c821daf70bd4..ea19ff2758af8 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -196,6 +196,7 @@ library , ghc-heap-view == 0.6.0 , directory + , semigroups >= 0.19.1 exposed-modules: Control.Arrow.Extended , Control.Arrow.Trans @@ -338,10 +339,11 @@ library , Hasura.GraphQL.Schema.Mutation.Delete , Hasura.GraphQL.Schema , Hasura.GraphQL.Utils + , Hasura.GraphQL.NormalForm , Hasura.GraphQL.Validate , Hasura.GraphQL.Validate.Types , Hasura.GraphQL.Validate.Context - , Hasura.GraphQL.Validate.Field + , Hasura.GraphQL.Validate.SelectionSet , Hasura.GraphQL.Validate.InputValue , Hasura.GraphQL.Explain , Hasura.GraphQL.Execute diff --git a/server/src-lib/Data/HashMap/Strict/InsOrd/Extended.hs b/server/src-lib/Data/HashMap/Strict/InsOrd/Extended.hs index 298e88663e8e1..832b74206022d 100644 --- a/server/src-lib/Data/HashMap/Strict/InsOrd/Extended.hs +++ b/server/src-lib/Data/HashMap/Strict/InsOrd/Extended.hs @@ -1,14 +1,14 @@ module Data.HashMap.Strict.InsOrd.Extended - ( OMap.elems + ( module OMap , groupTuples , groupListWith ) where -import qualified Data.HashMap.Strict.InsOrd as OMap +import Data.HashMap.Strict.InsOrd as OMap import qualified Data.Sequence.NonEmpty as NE +import qualified Data.List as L import Data.Hashable (Hashable) -import Data.List (foldl') import Prelude (Eq, Foldable, Functor, fmap, ($)) @@ -16,7 +16,7 @@ groupTuples :: (Eq k, Hashable k, Foldable t) => t (k, v) -> OMap.InsOrdHashMap k (NE.NESeq v) groupTuples = - foldl' groupFlds OMap.empty + L.foldl' groupFlds OMap.empty where groupFlds m (k, v) = OMap.insertWith (\_ c -> c NE.|> v) k (NE.init v) m diff --git a/server/src-lib/Data/Sequence/NonEmpty.hs b/server/src-lib/Data/Sequence/NonEmpty.hs index 6bc4548d5a512..a8f3820413f21 100644 --- a/server/src-lib/Data/Sequence/NonEmpty.hs +++ b/server/src-lib/Data/Sequence/NonEmpty.hs @@ -4,12 +4,14 @@ module Data.Sequence.NonEmpty , (|>) , init , head + , tail , toSeq ) where -import qualified Data.Foldable as F +import qualified Data.Foldable as Foldable import qualified Data.Sequence as Seq -import Prelude (Eq, Show, fst, (.)) +import qualified Data.Functor as Functor +import Prelude (Eq, Show, fst, snd, (.), Semigroup(..)) infixr 5 <| infixl 5 |> @@ -18,8 +20,12 @@ newtype NESeq a = NESeq { unNESeq :: (a, Seq.Seq a)} deriving (Show, Eq) -instance F.Foldable NESeq where - foldr f v = F.foldr f v . toSeq +instance Functor.Functor NESeq where + fmap f (NESeq (a, rest)) + = NESeq (f a, Functor.fmap f rest) + +instance Foldable.Foldable NESeq where + foldr f v = Foldable.foldr f v . toSeq init :: a -> NESeq a init a = NESeq (a, Seq.empty) @@ -27,6 +33,9 @@ init a = NESeq (a, Seq.empty) head :: NESeq a -> a head = fst . unNESeq +tail :: NESeq a -> Seq.Seq a +tail = snd . unNESeq + (|>) :: NESeq a -> a -> NESeq a (NESeq (h, l)) |> v = NESeq (h, l Seq.|> v) @@ -35,3 +44,7 @@ v <| (NESeq (h, l)) = NESeq (v, h Seq.<| l) toSeq :: NESeq a -> Seq.Seq a toSeq (NESeq (v, l)) = v Seq.<| l + +instance Semigroup (NESeq a) where + (NESeq (h, l)) <> r = + NESeq (h, l <> toSeq r) diff --git a/server/src-lib/Hasura/GraphQL/NormalForm.hs b/server/src-lib/Hasura/GraphQL/NormalForm.hs new file mode 100644 index 0000000000000..af7df978c4aa3 --- /dev/null +++ b/server/src-lib/Hasura/GraphQL/NormalForm.hs @@ -0,0 +1,229 @@ +{-# LANGUAGE TypeFamilyDependencies #-} +{-# LANGUAGE RecordWildCards #-} +module Hasura.GraphQL.NormalForm + ( Selection(..) + , DenormalizedSelection + , DenormalizedSelectionSet + , DenormalizedField + , SelectionSet(..) + , ArgsMap + , Field(..) + , Typename(..) + , IsField(..) + , toField + , AliasedFields(..) + , asObjectSelectionSet + , ObjectSelectionSet(..) + , ObjectSelectionSetMap + , traverseObjectSelectionSet + , InterfaceSelectionSet + , getMemberSelectionSet + , UnionSelectionSet + , ScopedSelectionSet(..) + , emptyScopedSelectionSet + , getUnionSelectionSet + , getInterfaceSelectionSet + , getObjectSelectionSet + + , AnnInpVal(..) + , AnnGValue(..) + , AnnGObject + , AnnGEnumValue(..) + , hasNullVal + , getAnnInpValKind + ) where + +import Hasura.Prelude + +import qualified Data.Aeson as J +import qualified Data.Aeson.Casing as J +import qualified Data.Aeson.TH as J +import qualified Data.HashMap.Strict as Map +import qualified Data.HashMap.Strict.InsOrd.Extended as OMap +import qualified Language.GraphQL.Draft.Syntax as G + +import Hasura.SQL.Types +import Hasura.SQL.Value +import qualified Hasura.RQL.Types.Column as RQL +import qualified Hasura.RQL.Types.Error as RQL + +data Selection f s + = SelectionField !G.Alias !f + | SelectionInlineFragmentSpread !s + | SelectionFragmentSpread !G.Name !s + deriving (Show, Eq) + +-- | What a processed G.SelectionSet should look like +type family DenormalizedSelectionSet a = s | s -> a + +-- | What a processed G.Field should look like +type family DenormalizedField a + +type DenormalizedSelection a + = Selection (DenormalizedField a) (DenormalizedSelectionSet a) + +-- | Ordered fields +newtype AliasedFields f + = AliasedFields { unAliasedFields :: OMap.InsOrdHashMap G.Alias f } + deriving (Show, Eq, Functor, Foldable, Traversable, Semigroup) + +newtype ObjectSelectionSet + = ObjectSelectionSet { unObjectSelectionSet :: AliasedFields Field } + deriving (Show, Eq, Semigroup) + +traverseObjectSelectionSet + :: (Monad m) => ObjectSelectionSet -> (Field -> m a) -> m [(Text, a)] +traverseObjectSelectionSet selectionSet f = + forM (OMap.toList $ unAliasedFields $ unObjectSelectionSet selectionSet) $ + \(alias, field) -> (G.unName $ G.unAlias alias,) <$> f field + +type ObjectSelectionSetMap + = Map.HashMap G.NamedType ObjectSelectionSet + +data Typename = Typename + deriving (Show, Eq, Generic) + +data ScopedSelectionSet f + = ScopedSelectionSet + { _sssBaseSelectionSet :: !(AliasedFields f) + -- ^ Fields that aren't explicitly defined for member types + , _sssMemberSelectionSets :: !ObjectSelectionSetMap + -- ^ SelectionSets of individual member types + } deriving (Show, Eq, Generic) + +emptyScopedSelectionSet :: ScopedSelectionSet f +emptyScopedSelectionSet = + ScopedSelectionSet (AliasedFields mempty) mempty + +type InterfaceSelectionSet = ScopedSelectionSet Field + +type UnionSelectionSet = ScopedSelectionSet Typename + +data SelectionSet + = SelectionSetObject !ObjectSelectionSet + | SelectionSetUnion !UnionSelectionSet + | SelectionSetInterface !InterfaceSelectionSet + | SelectionSetNone + -- ^ in cases of enums and scalars + deriving (Show, Eq) + +getObjectSelectionSet :: SelectionSet -> Maybe ObjectSelectionSet +getObjectSelectionSet = \case + SelectionSetObject s -> pure s + _ -> Nothing + +asObjectSelectionSet + :: (MonadError RQL.QErr m) => SelectionSet -> m ObjectSelectionSet +asObjectSelectionSet selectionSet = + onNothing (getObjectSelectionSet selectionSet) $ + RQL.throw500 "expecting ObjectSelectionSet" + +getUnionSelectionSet :: SelectionSet -> Maybe UnionSelectionSet +getUnionSelectionSet = \case + SelectionSetUnion s -> pure s + _ -> Nothing + +getInterfaceSelectionSet :: SelectionSet -> Maybe InterfaceSelectionSet +getInterfaceSelectionSet = \case + SelectionSetInterface s -> pure s + _ -> Nothing + +type ArgsMap = Map.HashMap G.Name AnnInpVal + +data Field + = Field + { _fName :: !G.Name + , _fType :: !G.NamedType + , _fArguments :: !ArgsMap + , _fSelSet :: !SelectionSet + } deriving (Eq, Show) + +-- $(J.deriveToJSON (J.aesonDrop 2 J.camelCase){J.omitNothingFields=True} +-- ''Field +-- ) + +-- $(J.deriveToJSON (J.aesonDrop 2 J.camelCase){J.omitNothingFields=True} +-- ''InterfaceSelectionSet +-- ) + +-- $(J.deriveToJSON (J.aesonDrop 2 J.camelCase){J.omitNothingFields=True} +-- ''SelectionSet +-- ) + +class IsField f where + getFieldName :: f -> G.Name + getFieldType :: f -> G.NamedType + getFieldArguments :: f -> ArgsMap + getFieldSelectionSet :: f -> SelectionSet + +toField :: (IsField f) => f -> Field +toField f = + Field (getFieldName f) (getFieldType f) + (getFieldArguments f) (getFieldSelectionSet f) + +instance IsField Field where + getFieldName = _fName + getFieldType = _fType + getFieldArguments = _fArguments + getFieldSelectionSet = _fSelSet + +instance IsField Typename where + getFieldName _ = "__typename" + getFieldType _ = G.NamedType "String" + getFieldArguments _ = mempty + getFieldSelectionSet _ = SelectionSetNone + +getMemberSelectionSet + :: IsField f + => G.NamedType -> ScopedSelectionSet f -> ObjectSelectionSet +getMemberSelectionSet namedType (ScopedSelectionSet {..}) = + fromMaybe (ObjectSelectionSet (fmap toField _sssBaseSelectionSet)) $ + Map.lookup namedType $ _sssMemberSelectionSets + +data AnnInpVal + = AnnInpVal + { _aivType :: !G.GType + , _aivVariable :: !(Maybe G.Variable) + , _aivValue :: !AnnGValue + } deriving (Show, Eq) + +type AnnGObject = OMap.InsOrdHashMap G.Name AnnInpVal + +-- | See 'EnumValuesInfo' for information about what these cases mean. +data AnnGEnumValue + = AGESynthetic !(Maybe G.EnumValue) + | AGEReference !RQL.EnumReference !(Maybe RQL.EnumValue) + deriving (Show, Eq) + +data AnnGValue + = AGScalar !PGScalarType !(Maybe PGScalarValue) + | AGEnum !G.NamedType !AnnGEnumValue + | AGObject !G.NamedType !(Maybe AnnGObject) + | AGArray !G.ListType !(Maybe [AnnInpVal]) + deriving (Show, Eq) + +$(J.deriveToJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True} + ''AnnInpVal + ) + +instance J.ToJSON AnnGValue where + -- toJSON (AGScalar ty valM) = + toJSON = const J.Null + -- J. + -- J.toJSON [J.toJSON ty, J.toJSON valM] + +hasNullVal :: AnnGValue -> Bool +hasNullVal = \case + AGScalar _ Nothing -> True + AGEnum _ (AGESynthetic Nothing) -> True + AGEnum _ (AGEReference _ Nothing) -> True + AGObject _ Nothing -> True + AGArray _ Nothing -> True + _ -> False + +getAnnInpValKind :: AnnGValue -> Text +getAnnInpValKind = \case + AGScalar _ _ -> "scalar" + AGEnum _ _ -> "enum" + AGObject _ _ -> "object" + AGArray _ _ -> "array" diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs b/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs index 436f7a85d52dd..4e851bacd2d3d 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs @@ -102,9 +102,9 @@ notBuiltinFld f = getImplTypes :: (MonadReader t m, Has TypeMap t) => AsObjType -> m [ObjTyInfo] getImplTypes aot = do - tyInfo :: TypeMap <- asks getter + tyInfo <- asks getter return $ sortOn _otiName $ - Map.elems $ getPossibleObjTypes' tyInfo aot + Map.elems $ getPossibleObjTypes tyInfo aot -- 4.5.2.3 unionR @@ -140,7 +140,7 @@ ifaceR' => IFaceTyInfo -> Field -> m J.Object -ifaceR' i@(IFaceTyInfo descM n flds) fld = +ifaceR' i@(IFaceTyInfo descM n flds implementations) fld = withSubFields (_fSelSet fld) $ \subFld -> case _fName subFld of "__typename" -> retJT "__Type" diff --git a/server/src-lib/Hasura/GraphQL/Validate.hs b/server/src-lib/Hasura/GraphQL/Validate.hs index d39f4c5bbc168..140fd5143614c 100644 --- a/server/src-lib/Hasura/GraphQL/Validate.hs +++ b/server/src-lib/Hasura/GraphQL/Validate.hs @@ -137,10 +137,8 @@ validateFrag validateFrag (G.FragmentDefinition n onTy dirs selSet) = do unless (null dirs) $ throwVE "unexpected directives at fragment definition" - tyInfo <- getTyInfoVE onTy - objTyInfo <- onNothing (getObjTyM tyInfo) $ throwVE - "fragments can only be defined on object types" - return $ FragDef n objTyInfo selSet + fragmentTypeInfo <- getFragmentTyInfo onTy + return $ FragDef n fragmentTypeInfo selSet data RootSelSet = RQuery !SelSet @@ -165,7 +163,7 @@ validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do -- build a validation ctx let valCtx = ValidationCtx (_gTypes ctx) annVarVals annFragDefs - selSet <- flip runReaderT valCtx $ denormSelSet [] opRoot $ + selSet <- flip runReaderT valCtx $ denormalizeSelectionSet [] opRoot $ G._todSelectionSet opDef case G._todType opDef of diff --git a/server/src-lib/Hasura/GraphQL/Validate/Context.hs b/server/src-lib/Hasura/GraphQL/Validate/Context.hs index b82c133812f67..a21d8e84d9924 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/Context.hs @@ -4,6 +4,7 @@ module Hasura.GraphQL.Validate.Context , getInpFieldInfo , getTyInfo , getTyInfoVE + , getFragmentTyInfo , module Hasura.GraphQL.Utils ) where @@ -19,11 +20,11 @@ import Hasura.RQL.Types getFieldInfo :: ( MonadError QErr m) - => ObjTyInfo -> G.Name -> m ObjFldInfo -getFieldInfo oti fldName = - onNothing (Map.lookup fldName $ _otiFields oti) $ throwVE $ + => G.NamedType -> ObjFieldMap -> G.Name -> m ObjFldInfo +getFieldInfo typeName fieldMap fldName = + onNothing (Map.lookup fldName fieldMap) $ throwVE $ "field " <> showName fldName <> - " not found in type: " <> showNamedTy (_otiName oti) + " not found in type: " <> showNamedTy typeName getInpFieldInfo :: ( MonadError QErr m) @@ -65,3 +66,13 @@ getTyInfoVE namedTy = do tyMap <- asks getter onNothing (Map.lookup namedTy tyMap) $ throwVE $ "no such type exists in the schema: " <> showNamedTy namedTy + +getFragmentTyInfo + :: (MonadReader r m, Has TypeMap r, MonadError QErr m) + => G.NamedType -> m FragmentTypeInfo +getFragmentTyInfo onType = + getTyInfoVE onType >>= \case + TIObj tyInfo -> pure $ FragmentTyObject tyInfo + TIIFace tyInfo -> pure $ FragmentTyInterface tyInfo + TIUnion tyInfo -> pure $ FragmentTyUnion tyInfo + _ -> throwVE "fragments can only be defined on object/interface/union types" diff --git a/server/src-lib/Hasura/GraphQL/Validate/Field.hs b/server/src-lib/Hasura/GraphQL/Validate/Field.hs deleted file mode 100644 index 46167d472f4a5..0000000000000 --- a/server/src-lib/Hasura/GraphQL/Validate/Field.hs +++ /dev/null @@ -1,350 +0,0 @@ -module Hasura.GraphQL.Validate.Field - ( ArgsMap - , Field(..) - , SelSet - , denormSelSet - ) where - -import Hasura.Prelude - -import qualified Data.Aeson as J -import qualified Data.Aeson.Casing as J -import qualified Data.Aeson.TH as J -import qualified Data.HashMap.Strict as Map -import qualified Data.HashMap.Strict.InsOrd.Extended as OMap -import qualified Data.List as L -import qualified Data.Sequence as Seq -import qualified Data.Sequence.NonEmpty as NE -import qualified Data.Text as T -import qualified Language.GraphQL.Draft.Syntax as G - -import Hasura.GraphQL.Validate.Context -import Hasura.GraphQL.Validate.InputValue -import Hasura.GraphQL.Validate.Types -import Hasura.RQL.Types -import Hasura.SQL.Value - --- data ScalarInfo --- = SIBuiltin !GBuiltin --- | SICustom !PGScalarType --- deriving (Show, Eq) - --- data GBuiltin --- = GInt --- | GFloat --- | GBoolean --- | GString --- deriving (Show, Eq) - -data TypedOperation - = TypedOperation - { _toType :: !G.OperationType - , _toName :: !(Maybe G.Name) - , _toSelectionSet :: ![Field] - } deriving (Show, Eq) - -type ArgsMap = Map.HashMap G.Name AnnInpVal - -type SelSet = Seq.Seq Field - -data Field - = Field - { _fAlias :: !G.Alias - , _fName :: !G.Name - , _fType :: !G.NamedType - , _fArguments :: !ArgsMap - , _fSelSet :: !SelSet - } deriving (Eq, Show) - -$(J.deriveToJSON (J.aesonDrop 2 J.camelCase){J.omitNothingFields=True} - ''Field - ) - --- newtype FieldMapAlias --- = FieldMapAlias --- { unFieldMapAlias :: Map.HashMap G.Alias (FieldG FieldMapAlias) --- } deriving (Show, Eq) - --- newtype FieldMapName --- = FieldMapName --- { unFieldMapName :: Map.HashMap G.Name (NE.NonEmpty (FieldG FieldMapName)) --- } deriving (Show, Eq) - --- type Field = FieldG FieldMapAlias - --- type FieldGrouped = FieldG FieldMapName - --- toFieldGrouped :: Field -> FieldGrouped --- toFieldGrouped = --- fmap groupFields --- where --- groupFields m = --- FieldMapName $ groupTuples $ --- flip map (Map.elems $ unFieldMapAlias m) $ \fld -> --- (_fName fld, toFieldGrouped fld) - -data FieldGroupSrc - = FGSFragSprd !G.Name - | FGSInlnFrag - deriving (Show, Eq) - -data FieldGroup - = FieldGroup - { _fgSource :: !FieldGroupSrc - , _fgFields :: !(Seq.Seq Field) - } deriving (Show, Eq) - --- data GLoc --- = GLoc --- { _glLine :: !Int --- , _glColumn :: !Int --- } deriving (Show, Eq) - --- data GErr --- = GErr --- { _geMessage :: !Text --- , _geLocations :: ![GLoc] --- } deriving (Show, Eq) - --- throwGE :: (MonadError QErr m) => Text -> m a --- throwGE msg = throwError $ QErr msg [] - -withDirectives - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => [G.Directive] - -> m a - -> m (Maybe a) -withDirectives dirs act = do - dirDefs <- onLeft (mkMapWith G._dName dirs) $ \dups -> - throwVE $ "the following directives are used more than once: " <> - showNames dups - - procDirs <- flip Map.traverseWithKey dirDefs $ \name dir -> - withPathK (G.unName name) $ do - dirInfo <- onNothing (Map.lookup (G._dName dir) defDirectivesMap) $ - throwVE $ "unexpected directive: " <> showName name - procArgs <- withPathK "args" $ processArgs (_diParams dirInfo) - (G._dArguments dir) - getIfArg procArgs - - let shouldSkip = fromMaybe False $ Map.lookup "skip" procDirs - shouldInclude = fromMaybe True $ Map.lookup "include" procDirs - - if not shouldSkip && shouldInclude - then Just <$> act - else return Nothing - - where - getIfArg m = do - val <- onNothing (Map.lookup "if" m) $ throw500 - "missing if argument in the directive" - when (isJust $ _aivVariable val) markNotReusable - case _aivValue val of - AGScalar _ (Just (PGValBoolean v)) -> return v - _ -> throw500 "did not find boolean scalar for if argument" - -denormSel - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => [G.Name] -- visited fragments - -> ObjTyInfo -- parent type info - -> G.Selection - -> m (Maybe (Either Field FieldGroup)) -denormSel visFrags parObjTyInfo sel = case sel of - G.SelectionField fld -> withPathK (G.unName $ G._fName fld) $ do - fldInfo <- getFieldInfo parObjTyInfo $ G._fName fld - fmap Left <$> denormFld visFrags fldInfo fld - G.SelectionFragmentSpread fragSprd -> - withPathK (G.unName $ G._fsName fragSprd) $ - fmap Right <$> denormFrag visFrags parTy fragSprd - G.SelectionInlineFragment inlnFrag -> - withPathK "inlineFragment" $ - fmap Right <$> denormInlnFrag visFrags parObjTyInfo inlnFrag - where - parTy = _otiName parObjTyInfo - -processArgs - :: ( MonadReader ValidationCtx m - , MonadError QErr m - ) - => ParamMap - -> [G.Argument] - -> m ArgsMap -processArgs fldParams argsL = do - - args <- onLeft (mkMapWith G._aName argsL) $ \dups -> - throwVE $ "the following arguments are defined more than once: " <> - showNames dups - - let requiredParams = Map.filter (G.isNotNull . _iviType) fldParams - - inpArgs <- forM args $ \(G.Argument argName argVal) -> - withPathK (G.unName argName) $ do - argTy <- getArgTy argName - validateInputValue valueParser argTy argVal - - forM_ requiredParams $ \argDef -> do - let param = _iviName argDef - onNothing (Map.lookup param inpArgs) $ throwVE $ mconcat - [ "the required argument ", showName param, " is missing"] - - return inpArgs - - where - getArgTy argName = - onNothing (_iviType <$> Map.lookup argName fldParams) $ throwVE $ - "no such argument " <> showName argName <> " is expected" - -denormFld - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => [G.Name] -- visited fragments - -> ObjFldInfo - -> G.Field - -> m (Maybe Field) -denormFld visFrags fldInfo (G.Field aliasM name args dirs selSet) = do - - let fldTy = _fiTy fldInfo - fldBaseTy = getBaseTy fldTy - - fldTyInfo <- getTyInfo fldBaseTy - - argMap <- withPathK "args" $ processArgs (_fiParams fldInfo) args - - fields <- case (fldTyInfo, selSet) of - - (TIObj _, []) -> - throwVE $ "field " <> showName name <> " of type " - <> G.showGT fldTy <> " must have a selection of subfields" - - (TIObj fldObjTyInfo, _) -> - denormSelSet visFrags fldObjTyInfo selSet - - (TIScalar _, []) -> return Seq.empty - (TIEnum _, []) -> return Seq.empty - - (TIInpObj _, _) -> - throwVE $ "internal error: unexpected input type for field: " - <> showName name - - -- when scalar/enum and no empty set - (_, _) -> - throwVE $ "field " <> showName name <> " must not have a " - <> "selection since type " <> G.showGT fldTy <> " has no subfields" - - withPathK "directives" $ withDirectives dirs $ return $ - Field (fromMaybe (G.Alias name) aliasM) name fldBaseTy argMap fields - -denormInlnFrag - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => [G.Name] -- visited fragments - -> ObjTyInfo -- type information of the field - -> G.InlineFragment - -> m (Maybe FieldGroup) -denormInlnFrag visFrags fldTyInfo inlnFrag = do - let fldTy = _otiName fldTyInfo - let fragTy = fromMaybe fldTy tyM - when (fldTy /= fragTy) $ - throwVE $ "inline fragment is expected on type " <> - showNamedTy fldTy <> " but found " <> showNamedTy fragTy - withPathK "directives" $ withDirectives directives $ - fmap (FieldGroup FGSInlnFrag) $ denormSelSet visFrags fldTyInfo selSet - where - G.InlineFragment tyM directives selSet = inlnFrag - -denormSelSet - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => [G.Name] -- visited fragments - -> ObjTyInfo - -> G.SelectionSet - -> m (Seq.Seq Field) -denormSelSet visFrags fldTyInfo selSet = - withPathK "selectionSet" $ do - resFlds <- catMaybes <$> mapM (denormSel visFrags fldTyInfo) selSet - mergeFields $ foldl' flatten Seq.empty resFlds - where - flatten s (Left fld) = s Seq.|> fld - flatten s (Right (FieldGroup _ flds)) = - s Seq.>< flds - -mergeFields - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => Seq.Seq Field - -> m (Seq.Seq Field) -mergeFields flds = - fmap Seq.fromList $ forM fldGroups $ \fieldGroup -> do - newFld <- checkMergeability fieldGroup - childFields <- mergeFields $ foldl' (\l f -> l Seq.>< _fSelSet f) Seq.empty - $ NE.toSeq fieldGroup - return $ newFld {_fSelSet = childFields} - where - fldGroups = OMap.elems $ OMap.groupListWith _fAlias flds - -- can a group be merged? - checkMergeability fldGroup = do - let groupedFlds = toList $ NE.toSeq fldGroup - fldNames = L.nub $ map _fName groupedFlds - args = L.nub $ map _fArguments groupedFlds - fld = NE.head fldGroup - fldAl = _fAlias fld - when (length fldNames > 1) $ - throwVE $ "cannot merge different fields under the same alias (" - <> showName (G.unAlias fldAl) <> "): " - <> showNames fldNames - when (length args > 1) $ - throwVE $ "cannot merge fields with different arguments" - <> " under the same alias: " - <> showName (G.unAlias fldAl) - return fld - -denormFrag - :: ( MonadReader ValidationCtx m - , MonadError QErr m - , MonadReusability m - ) - => [G.Name] -- visited fragments - -> G.NamedType -- parent type - -> G.FragmentSpread - -> m (Maybe FieldGroup) -denormFrag visFrags parTy (G.FragmentSpread name directives) = do - - -- check for cycles - when (name `elem` visFrags) $ - throwVE $ "cannot spread fragment " <> showName name - <> " within itself via " - <> T.intercalate "," (map G.unName visFrags) - - (FragDef _ fragTyInfo selSet) <- getFragInfo - - let fragTy = _otiName fragTyInfo - - -- we don't have unions or interfaces so we can get away with equality - when (fragTy /= parTy) $ - throwVE $ "cannot spread fragment " <> showName name <> " defined on " <> - showNamedTy fragTy <> " when selecting fields of type " <> showNamedTy parTy - - resFlds <- denormSelSet (name:visFrags) fragTyInfo selSet - - withPathK "directives" $ withDirectives directives $ - return $ FieldGroup (FGSFragSprd name) resFlds - - where - getFragInfo = do - dctx <- ask - onNothing (Map.lookup name $ _vcFragDefMap dctx) $ - throwVE $ "fragment '" <> G.unName name <> "' not found" diff --git a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs new file mode 100644 index 0000000000000..b653e55012c37 --- /dev/null +++ b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs @@ -0,0 +1,578 @@ +{-# LANGUAGE TypeFamilyDependencies #-} +{-# LANGUAGE RecordWildCards #-} +module Hasura.GraphQL.Validate.SelectionSet + ( ArgsMap + , Field(..) + , AliasedFields(..) + , SelectionSet(..) + , ObjectSelectionSet(..) + , traverseObjectSelectionSet + , InterfaceSelectionSet + , UnionSelectionSet + -- , denormalizeSelectionSet + , denormalizeObjectSelectionSet + , asObjectSelectionSet + ) where + +import Hasura.Prelude + +import qualified Data.Text as T +import qualified Data.HashMap.Strict as Map +import qualified Data.HashSet as Set +import qualified Data.HashMap.Strict.InsOrd.Extended as OMap +import qualified Language.GraphQL.Draft.Syntax as G +import qualified Data.Sequence.NonEmpty as NE +import qualified Data.List as L + +import Hasura.GraphQL.Validate.Context +import Hasura.GraphQL.Validate.Types +import Hasura.GraphQL.Validate.InputValue +import Hasura.GraphQL.NormalForm +import Hasura.RQL.Types +import Hasura.SQL.Value + +class HasSelectionSet a where + + getTypename :: a -> G.NamedType + getMemberTypes :: a -> Set.HashSet G.NamedType + + denormalizeField_ + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + , MonadState [G.Name] m + ) + => a + -> G.Field + -> m (Maybe (DenormalizedField a)) + + mergeSelections + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + ) + => [Selection (DenormalizedField a) (DenormalizedSelectionSet a)] + -> m (DenormalizedSelectionSet a) + + fromObjectSelectionSet + :: G.NamedType + -- ^ parent typename + -> G.NamedType + -- ^ fragment typename + -> Set.HashSet G.NamedType + -> DenormalizedSelectionSet ObjTyInfo + -> DenormalizedSelectionSet a + + fromInterfaceSelectionSet + :: G.NamedType + -- ^ parent typename + -> G.NamedType + -- ^ fragment typename + -> Set.HashSet G.NamedType + -> DenormalizedSelectionSet IFaceTyInfo + -> DenormalizedSelectionSet a + + fromUnionSelectionSet + :: G.NamedType + -- ^ parent typename + -> G.NamedType + -- ^ fragment typename + -> Set.HashSet G.NamedType + -> DenormalizedSelectionSet UnionTyInfo + -> DenormalizedSelectionSet a + +denormalizeObjectSelectionSet + :: ( MonadError QErr m + , MonadReusability m + ) + => ValidationCtx + -> ObjTyInfo + -> G.SelectionSet + -> m ObjectSelectionSet +denormalizeObjectSelectionSet validationCtx objectTypeInfo selectionSet = + flip evalStateT [] $ flip runReaderT validationCtx $ + denormalizeSelectionSet objectTypeInfo selectionSet + +denormalizeSelectionSet + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + , HasSelectionSet a + , MonadState [G.Name] m + ) + => a + -> G.SelectionSet + -> m (DenormalizedSelectionSet a) +denormalizeSelectionSet fieldTypeInfo selectionSet = + withPathK "selectionSet" $ do + resolvedSelections <- catMaybes <$> + mapM (denormalizeSelection fieldTypeInfo) selectionSet + mergeSelections resolvedSelections + +-- | While interfaces and objects have fields, unions do not, so +-- this is a specialized function for every Object type +denormalizeSelection + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + , MonadState [G.Name] m + , HasSelectionSet a + ) + => a -- parent type info + -> G.Selection + -> m (Maybe (DenormalizedSelection a)) +denormalizeSelection parentTypeInfo = \case + G.SelectionField fld -> withPathK (G.unName $ G._fName fld) $ do + let fieldName = G._fName fld + fieldAlias = fromMaybe (G.Alias fieldName) $ G._fAlias fld + fmap (SelectionField fieldAlias) <$> denormalizeField_ parentTypeInfo fld + G.SelectionFragmentSpread (G.FragmentSpread name directives) -> do + FragDef _ fragmentTyInfo fragmentSelectionSet <- getFragmentInfo name + withPathK (G.unName name) $ + fmap (SelectionFragmentSpread name) <$> + denormalizeFragment parentTypeInfo fragmentTyInfo directives fragmentSelectionSet + G.SelectionInlineFragment (G.InlineFragment {..}) -> do + let fragmentType = fromMaybe (getTypename parentTypeInfo) _ifTypeCondition + fragmentTyInfo <- getFragmentTyInfo fragmentType + withPathK "inlineFragment" $ fmap SelectionInlineFragmentSpread <$> + denormalizeFragment parentTypeInfo fragmentTyInfo _ifDirectives _ifSelectionSet + +denormalizeFragment + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + , MonadState [G.Name] m + , HasSelectionSet a + ) + => a + -> FragmentTypeInfo + -> [G.Directive] + -> G.SelectionSet + -> m (Maybe (DenormalizedSelectionSet a)) +denormalizeFragment parentTyInfo fragmentTyInfo directives fragmentSelectionSet = do + commonTypes <- validateSpread + case fragmentTyInfo of + FragmentTyObject objTyInfo -> + withDirectives directives $ + fmap (fromObjectSelectionSet parentType fragmentType commonTypes) $ + denormalizeSelectionSet objTyInfo fragmentSelectionSet + FragmentTyInterface interfaceTyInfo -> + withDirectives directives $ + fmap (fromInterfaceSelectionSet parentType fragmentType commonTypes) $ + denormalizeSelectionSet interfaceTyInfo fragmentSelectionSet + FragmentTyUnion unionTyInfo -> + withDirectives directives $ + fmap (fromUnionSelectionSet parentType fragmentType commonTypes) $ + denormalizeSelectionSet unionTyInfo fragmentSelectionSet + where + validateSpread = do + let commonTypes = parentTypeMembers `Set.intersection` fragmentTypeMembers + if null commonTypes then + -- TODO: fragment source + -- throwVE $ "cannot spread fragment " <> showName name <> " defined on " <> + throwVE $ "cannot spread fragment defined on " <> showNamedTy fragmentType + <> " when selecting fields of type " <> showNamedTy parentType + else pure commonTypes + + parentType = getTypename parentTyInfo + parentTypeMembers = getMemberTypes parentTyInfo + + fragmentType = case fragmentTyInfo of + FragmentTyObject tyInfo -> getTypename tyInfo + FragmentTyInterface tyInfo -> getTypename tyInfo + FragmentTyUnion tyInfo -> getTypename tyInfo + fragmentTypeMembers = case fragmentTyInfo of + FragmentTyObject tyInfo -> getMemberTypes tyInfo + FragmentTyInterface tyInfo -> getMemberTypes tyInfo + FragmentTyUnion tyInfo -> getMemberTypes tyInfo + +class IsField f => MergeableField f where + + checkFieldMergeability + :: (MonadError QErr m) => G.Alias -> NE.NESeq f -> m f + +instance MergeableField Field where + + checkFieldMergeability alias fields = do + let groupedFlds = toList $ NE.toSeq fields + fldNames = L.nub $ map getFieldName groupedFlds + args = L.nub $ map getFieldArguments groupedFlds + when (length fldNames > 1) $ + throwVE $ "cannot merge different fields under the same alias (" + <> showName (G.unAlias alias) <> "): " + <> showNames fldNames + when (length args > 1) $ + throwVE $ "cannot merge fields with different arguments" + <> " under the same alias: " + <> showName (G.unAlias alias) + let fld = NE.head fields + mergedGroupSelectionSet <- mergeSelectionSets $ fmap _fSelSet fields + return $ fld { _fSelSet = mergedGroupSelectionSet } + +instance MergeableField Typename where + + checkFieldMergeability _ fields = pure $ NE.head fields + +parseArguments + :: ( MonadReader ValidationCtx m + , MonadError QErr m + ) + => ParamMap + -> [G.Argument] + -> m ArgsMap +parseArguments fldParams argsL = do + + args <- onLeft (mkMapWith G._aName argsL) $ \dups -> + throwVE $ "the following arguments are defined more than once: " <> + showNames dups + + let requiredParams = Map.filter (G.isNotNull . _iviType) fldParams + + inpArgs <- forM args $ \(G.Argument argName argVal) -> + withPathK (G.unName argName) $ do + argTy <- getArgTy argName + validateInputValue valueParser argTy argVal + + forM_ requiredParams $ \argDef -> do + let param = _iviName argDef + onNothing (Map.lookup param inpArgs) $ throwVE $ mconcat + [ "the required argument ", showName param, " is missing"] + + return inpArgs + + where + getArgTy argName = + onNothing (_iviType <$> Map.lookup argName fldParams) $ throwVE $ + "no such argument " <> showName argName <> " is expected" + +mergeFields + :: ( MonadError QErr m + , MergeableField f + ) + -- => Seq.Seq Field + => [AliasedFields f] + -> m (AliasedFields f) +mergeFields flds = + AliasedFields <$> OMap.traverseWithKey checkFieldMergeability groups + where + -- groups :: OMap.InsOrdHashMap G.Alias (NE.NESeq f) + groups = foldr (OMap.unionWith (<>)) mempty $ + map (fmap NE.init . unAliasedFields) flds + +-- query q { +-- author { +-- id +-- } +-- author { +-- name +-- } +-- } +-- +-- | When we are merging two selection sets down two different trees they +-- should be of the same type, however, as it is not enforced in the type +-- system, an internal error is thrown when this assumption is violated +mergeSelectionSets + :: (MonadError QErr m) => NE.NESeq SelectionSet -> m SelectionSet +-- mergeSelectionSets = curry $ \case +mergeSelectionSets selectionSets = + case NE.head selectionSets of + SelectionSetObject s -> + SelectionSetObject <$> + assertingSimilar s getObjectSelectionSet mergeObjectSelectionSets + SelectionSetUnion s -> + SelectionSetUnion <$> + assertingSimilar s getUnionSelectionSet mergeUnionSelectionSets + SelectionSetInterface s -> + SelectionSetInterface <$> + assertingSimilar s getInterfaceSelectionSet mergeInterfaceSelectionSets + SelectionSetNone -> + if all (== SelectionSetNone) $ NE.tail selectionSets + then pure SelectionSetNone + else throw500 $ "mergeSelectionSets: 'same kind' assertion failed" + where + assertingSimilar + :: MonadError QErr m + => s -> (SelectionSet -> Maybe s) -> ([s] -> m s) -> m s + assertingSimilar s l f = do + let sameSelectionSets = mapMaybe l $ toList $ NE.tail selectionSets + if length sameSelectionSets == length (NE.tail selectionSets) + then f (s:sameSelectionSets) + else throw500 $ "mergeSelectionSets: 'same kind' assertion failed" + +mergeObjectSelectionSets + :: (MonadError QErr m) => [ObjectSelectionSet] -> m ObjectSelectionSet +mergeObjectSelectionSets = + fmap ObjectSelectionSet . mergeFields . map unObjectSelectionSet + +mergeObjectSelectionSetMaps + :: (MonadError QErr m) => [ObjectSelectionSetMap] -> m ObjectSelectionSetMap +mergeObjectSelectionSetMaps selectionSetMaps = + traverse mergeObjectSelectionSets $ + foldr (Map.unionWith (<>)) mempty $ map (fmap (:[])) selectionSetMaps + +appendScopedSelectionSet + :: (MonadError QErr m, MergeableField f) + => ScopedSelectionSet f -> ScopedSelectionSet f -> m (ScopedSelectionSet f) +appendScopedSelectionSet s1 s2 = + ScopedSelectionSet + <$> mergeFields [_sssBaseSelectionSet s1, _sssBaseSelectionSet s2] + <*> mergeObjectSelectionSetMaps [s1MembersUnified, s2MembersUnified] + + where + s1Base = fmap toField $ _sssBaseSelectionSet s1 + s2Base = fmap toField $ _sssBaseSelectionSet s2 + + s1MembersUnified = + (_sssMemberSelectionSets s1) + <> fmap (const (ObjectSelectionSet s1Base)) (_sssMemberSelectionSets s2) + + s2MembersUnified = + (_sssMemberSelectionSets s2) + <> fmap (const (ObjectSelectionSet s2Base)) (_sssMemberSelectionSets s1) + +mergeScopedSelectionSets + :: (MonadError QErr m, MergeableField f) + => [ScopedSelectionSet f] -> m (ScopedSelectionSet f) +mergeScopedSelectionSets selectionSets = + foldM appendScopedSelectionSet emptyScopedSelectionSet selectionSets + +mergeInterfaceSelectionSets + :: (MonadError QErr m) => [InterfaceSelectionSet] -> m InterfaceSelectionSet +mergeInterfaceSelectionSets = mergeScopedSelectionSets + +mergeUnionSelectionSets + :: (MonadError QErr m) => [UnionSelectionSet] -> m UnionSelectionSet +mergeUnionSelectionSets = mergeScopedSelectionSets + +withDirectives + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + ) + => [G.Directive] + -> m a + -> m (Maybe a) +withDirectives dirs act = withPathK "directives" $ do + dirDefs <- onLeft (mkMapWith G._dName dirs) $ \dups -> + throwVE $ "the following directives are used more than once: " <> + showNames dups + + procDirs <- flip Map.traverseWithKey dirDefs $ \name dir -> + withPathK (G.unName name) $ do + dirInfo <- onNothing (Map.lookup (G._dName dir) defDirectivesMap) $ + throwVE $ "unexpected directive: " <> showName name + procArgs <- withPathK "args" $ parseArguments (_diParams dirInfo) + (G._dArguments dir) + getIfArg procArgs + + let shouldSkip = fromMaybe False $ Map.lookup "skip" procDirs + shouldInclude = fromMaybe True $ Map.lookup "include" procDirs + + if not shouldSkip && shouldInclude + then Just <$> act + else return Nothing + + where + getIfArg m = do + val <- onNothing (Map.lookup "if" m) $ throw500 + "missing if argument in the directive" + when (isJust $ _aivVariable val) markNotReusable + case _aivValue val of + AGScalar _ (Just (PGValBoolean v)) -> return v + _ -> throw500 "did not find boolean scalar for if argument" + +getFragmentInfo + :: (MonadReader ValidationCtx m, MonadError QErr m, MonadState [G.Name] m) + => G.Name + -- ^ fragment name + -> m FragDef +getFragmentInfo name = do + -- check for cycles + visitedFragments <- get + if name `elem` visitedFragments + then throwVE $ "cannot spread fragment " <> showName name + <> " within itself via " + <> T.intercalate "," (map G.unName visitedFragments) + else put $ name:visitedFragments + fragInfo <- Map.lookup name <$> asks _vcFragDefMap + onNothing fragInfo $ throwVE $ "fragment '" <> G.unName name <> "' not found" + +denormalizeField + :: ( MonadReader ValidationCtx m + , MonadError QErr m + , MonadReusability m + , MonadState [G.Name] m + ) + => ObjFldInfo + -> G.Field + -> m (Maybe Field) +denormalizeField fldInfo (G.Field _ name args dirs selSet) = do + + let fldTy = _fiTy fldInfo + fldBaseTy = getBaseTy fldTy + + fldTyInfo <- getTyInfo fldBaseTy + + argMap <- withPathK "args" $ parseArguments (_fiParams fldInfo) args + + fields <- case (fldTyInfo, selSet) of + + (TIObj _, []) -> + throwVE $ "field " <> showName name <> " of type " + <> G.showGT fldTy <> " must have a selection of subfields" + + (TIObj objTyInfo, _) -> + SelectionSetObject <$> denormalizeSelectionSet objTyInfo selSet + + (TIIFace _, []) -> + throwVE $ "field " <> showName name <> " of type " + <> G.showGT fldTy <> " must have a selection of subfields" + + (TIIFace interfaceTyInfo, _) -> + SelectionSetInterface <$> denormalizeSelectionSet interfaceTyInfo selSet + + (TIUnion _, []) -> + throwVE $ "field " <> showName name <> " of type " + <> G.showGT fldTy <> " must have a selection of subfields" + + (TIUnion unionTyInfo, _) -> + SelectionSetUnion <$> denormalizeSelectionSet unionTyInfo selSet + + (TIScalar _, []) -> return SelectionSetNone + -- when scalar/enum and no empty set + (TIScalar _, _) -> + throwVE $ "field " <> showName name <> " must not have a " + <> "selection since type " <> G.showGT fldTy <> " has no subfields" + + (TIEnum _, []) -> return SelectionSetNone + (TIEnum _, _) -> + throwVE $ "field " <> showName name <> " must not have a " + <> "selection since type " <> G.showGT fldTy <> " has no subfields" + + (TIInpObj _, _) -> + throwVE $ "internal error: unexpected input type for field: " + <> showName name + + withDirectives dirs $ pure $ Field name fldBaseTy argMap fields + +-- inlineFragmentInObjectScope +-- :: ( MonadReader ValidationCtx m +-- , MonadError QErr m +-- , MonadReusability m +-- , MonadState [G.Name] m +-- ) +-- => ObjTyInfo -- type information of the field +-- -> G.InlineFragment +-- -> m (Maybe ObjectSelectionSet) +-- inlineFragmentInObjectScope fldTyInfo inlnFrag = do +-- let fldTy = _otiName fldTyInfo +-- let fragmentType = fromMaybe fldTy tyM +-- when (fldTy /= fragmentType) $ +-- throwVE $ "inline fragment is expected on type " <> +-- showNamedTy fldTy <> " but found " <> showNamedTy fragmentType +-- withDirectives directives $ denormalizeObjectSelectionSet fldTyInfo selSet +-- where +-- G.InlineFragment tyM directives selSet = inlnFrag + +type instance DenormalizedSelectionSet ObjTyInfo = ObjectSelectionSet +type instance DenormalizedField ObjTyInfo = Field +instance HasSelectionSet ObjTyInfo where + + getTypename = _otiName + getMemberTypes = Set.singleton . _otiName + + denormalizeField_ objTyInfo field = do + fieldInfo <- getFieldInfo (_otiName objTyInfo) (_otiFields objTyInfo) $ G._fName field + denormalizeField fieldInfo field + + mergeSelections selections = + mergeObjectSelectionSets $ map toObjectSelectionSet selections + where + toObjectSelectionSet = \case + SelectionField alias fld -> + ObjectSelectionSet $ AliasedFields $ OMap.singleton alias fld + SelectionInlineFragmentSpread selectionSet -> selectionSet + SelectionFragmentSpread _ selectionSet -> selectionSet + + fromObjectSelectionSet _ _ _ objectSelectionSet = + objectSelectionSet + + fromInterfaceSelectionSet parentType _ _ interfaceSelectionSet = + getMemberSelectionSet parentType interfaceSelectionSet + + fromUnionSelectionSet parentType _ _ unionSelectionSet = + getMemberSelectionSet parentType unionSelectionSet + +type instance DenormalizedSelectionSet IFaceTyInfo = InterfaceSelectionSet +type instance DenormalizedField IFaceTyInfo = Field + +instance HasSelectionSet IFaceTyInfo where + + getTypename = _ifName + -- TODO + getMemberTypes = _ifMemberTypes + + denormalizeField_ interfaceTyInfo field = do + fieldInfo <- getFieldInfo (_ifName interfaceTyInfo) (_ifFields interfaceTyInfo) + $ G._fName field + denormalizeField fieldInfo field + + mergeSelections selections = + mergeInterfaceSelectionSets $ map toInterfaceSelectionSet selections + where + toInterfaceSelectionSet = \case + SelectionField alias fld -> + ScopedSelectionSet (AliasedFields $ OMap.singleton alias fld) mempty + SelectionInlineFragmentSpread selectionSet -> selectionSet + SelectionFragmentSpread _ selectionSet -> selectionSet + + fromObjectSelectionSet _ fragmentType _ objectSelectionSet = + ScopedSelectionSet (AliasedFields mempty) $ + Map.singleton fragmentType objectSelectionSet + + fromInterfaceSelectionSet _ _ commonTypes interfaceSelectionSet = + ScopedSelectionSet (AliasedFields mempty) $ + Map.fromList $ flip map (toList commonTypes) $ + \commonType -> (commonType, getMemberSelectionSet commonType interfaceSelectionSet) + + fromUnionSelectionSet _ _ commonTypes unionSelectionSet = + ScopedSelectionSet (AliasedFields mempty) $ + Map.fromList $ flip map (toList commonTypes) $ + \commonType -> (commonType, getMemberSelectionSet commonType unionSelectionSet) + +type instance DenormalizedSelectionSet UnionTyInfo = UnionSelectionSet +type instance DenormalizedField UnionTyInfo = Typename + +instance HasSelectionSet UnionTyInfo where + + getTypename = _utiName + getMemberTypes = _utiMemberTypes + + denormalizeField_ unionTyInfo field = do + let fieldMap = Map.singleton (_fiName typenameFld) typenameFld + fieldInfo <- getFieldInfo (_utiName unionTyInfo) fieldMap $ G._fName field + fmap (const Typename) <$> denormalizeField fieldInfo field + + mergeSelections selections = + mergeUnionSelectionSets $ map toUnionSelectionSet selections + where + toUnionSelectionSet = \case + SelectionField alias fld -> + ScopedSelectionSet (AliasedFields $ OMap.singleton alias fld) mempty + SelectionInlineFragmentSpread selectionSet -> selectionSet + SelectionFragmentSpread _ selectionSet -> selectionSet + + fromObjectSelectionSet _ fragmentType _ objectSelectionSet = + ScopedSelectionSet (AliasedFields mempty) $ + Map.singleton fragmentType objectSelectionSet + + fromInterfaceSelectionSet _ _ commonTypes interfaceSelectionSet = + ScopedSelectionSet (AliasedFields mempty) $ + Map.fromList $ flip map (toList commonTypes) $ + \commonType -> (commonType, getMemberSelectionSet commonType interfaceSelectionSet) + + fromUnionSelectionSet _ _ commonTypes unionSelectionSet = + ScopedSelectionSet (AliasedFields mempty) $ + Map.fromList $ flip map (toList commonTypes) $ + \commonType -> (commonType, getMemberSelectionSet commonType unionSelectionSet) + diff --git a/server/src-lib/Hasura/GraphQL/Validate/Types.hs b/server/src-lib/Hasura/GraphQL/Validate/Types.hs index 83c7e8261867e..c7d17a6989a70 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/Types.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/Types.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE GADTs #-} module Hasura.GraphQL.Validate.Types ( InpValInfo(..) , ParamMap + , typenameFld , ObjFldInfo(..) , mkHsraObjFldInfo , ObjFieldMap @@ -20,6 +22,7 @@ module Hasura.GraphQL.Validate.Types , IFacesSet , UnionTyInfo(..) , FragDef(..) + , FragmentTypeInfo(..) , FragDefMap , AnnVarVals , AnnInpVal(..) @@ -46,7 +49,7 @@ module Hasura.GraphQL.Validate.Types , TypeInfo(..) , isObjTy , isIFaceTy - , getPossibleObjTypes' + , getPossibleObjTypes , getObjTyM , getUnionTyM , mkScalarTy @@ -54,7 +57,6 @@ module Hasura.GraphQL.Validate.Types , getNamedTy , mkTyInfoMap , fromTyDef - , fromTyDefQ , fromSchemaDoc , fromSchemaDocQ , TypeMap @@ -88,7 +90,6 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Casing as J import qualified Data.Aeson.TH as J import qualified Data.HashMap.Strict as Map -import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.HashSet as Set import qualified Data.Text as T import qualified Language.GraphQL.Draft.Syntax as G @@ -100,6 +101,7 @@ import Control.Lens (makePrisms) import qualified Hasura.RQL.Types.Column as RQL import Hasura.GraphQL.Utils +import Hasura.GraphQL.NormalForm import Hasura.RQL.Instances () import Hasura.RQL.Types.RemoteSchema import Hasura.SQL.Types @@ -126,10 +128,11 @@ fromEnumValDef (G.EnumValueDefinition descM val _) = data EnumValuesInfo = EnumValuesSynthetic !(Map.HashMap G.EnumValue EnumValInfo) - -- ^ Values for an enum that exists only in the GraphQL schema and does not have any external - -- source of truth. + -- ^ Values for an enum that exists only in the GraphQL schema and does not + -- have any external source of truth. | EnumValuesReference !RQL.EnumReference - -- ^ Values for an enum that is backed by an enum table reference (see "Hasura.RQL.Schema.Enum"). + -- ^ Values for an enum that is backed by an enum table reference (see + -- "Hasura.RQL.Schema.Enum"). deriving (Show, Eq, TH.Lift) normalizeEnumValues :: EnumValuesInfo -> Map.HashMap G.EnumValue EnumValInfo @@ -257,10 +260,11 @@ instance Semigroup ObjTyInfo where } mkObjTyInfo - :: Maybe G.Description -> G.NamedType -> IFacesSet -> ObjFieldMap -> TypeLoc -> ObjTyInfo + :: Maybe G.Description -> G.NamedType + -> IFacesSet -> ObjFieldMap -> TypeLoc -> ObjTyInfo mkObjTyInfo descM ty iFaces flds loc = ObjTyInfo descM ty iFaces $ Map.insert (_fiName newFld) newFld flds - where newFld = typenameFld loc + where newFld = typenameFld mkHsraObjTyInfo :: Maybe G.Description @@ -273,15 +277,16 @@ mkHsraObjTyInfo descM ty implIFaces flds = mkIFaceTyInfo :: Maybe G.Description -> G.NamedType - -> Map.HashMap G.Name ObjFldInfo -> TypeLoc -> IFaceTyInfo + -> Map.HashMap G.Name ObjFldInfo -> TypeLoc -> MemberTypes -> IFaceTyInfo mkIFaceTyInfo descM ty flds loc = IFaceTyInfo descM ty $ Map.insert (_fiName newFld) newFld flds - where newFld = typenameFld loc + where + newFld = typenameFld -typenameFld :: TypeLoc -> ObjFldInfo -typenameFld loc = +typenameFld :: ObjFldInfo +typenameFld = ObjFldInfo (Just desc) "__typename" Map.empty - (G.toGT $ G.toNT $ G.NamedType "String") loc + (G.toGT $ G.toNT $ G.NamedType "String") TLHasuraType where desc = "The name of the current Object type at runtime" @@ -293,9 +298,10 @@ fromObjTyDef (G.ObjectTypeDefinition descM n ifaces _ flds) loc = data IFaceTyInfo = IFaceTyInfo - { _ifDesc :: !(Maybe G.Description) - , _ifName :: !G.NamedType - , _ifFields :: !ObjFieldMap + { _ifDesc :: !(Maybe G.Description) + , _ifName :: !G.NamedType + , _ifFields :: !ObjFieldMap + , _ifMemberTypes :: !MemberTypes } deriving (Show, Eq, TH.Lift) instance EquatableGType IFaceTyInfo where @@ -303,19 +309,18 @@ instance EquatableGType IFaceTyInfo where (G.NamedType, Map.HashMap G.Name (G.Name, G.GType, ParamMap)) getEqProps a = (,) (_ifName a) (Map.map getEqProps (_ifFields a)) -instance Monoid IFaceTyInfo where - mempty = IFaceTyInfo Nothing (G.NamedType "") Map.empty - instance Semigroup IFaceTyInfo where objA <> objB = objA { _ifFields = Map.union (_ifFields objA) (_ifFields objB) } -fromIFaceDef :: G.InterfaceTypeDefinition -> TypeLoc -> IFaceTyInfo -fromIFaceDef (G.InterfaceTypeDefinition descM n _ flds) loc = - mkIFaceTyInfo descM (G.NamedType n) fldMap loc +fromIFaceDef + :: InterfaceImplementations -> G.InterfaceTypeDefinition -> TypeLoc -> IFaceTyInfo +fromIFaceDef interfaceImplementations (G.InterfaceTypeDefinition descM n _ flds) loc = + mkIFaceTyInfo descM (G.NamedType n) fldMap loc implementations where - fldMap = Map.fromList [(G._fldName fld, fromFldDef fld loc) | fld <- flds] + fldMap = Map.fromList [(G._fldName fld, fromFldDef fld loc) | fld <- flds] + implementations = fromMaybe mempty $ Map.lookup (G.NamedType n) interfaceImplementations type MemberTypes = Set.HashSet G.NamedType @@ -414,23 +419,23 @@ data TypeInfo instance J.ToJSON TypeInfo where toJSON _ = J.String "toJSON not implemented for TypeInfo" -instance J.FromJSON TypeInfo where - parseJSON _ = fail "FromJSON not implemented for TypeInfo" - data AsObjType - = AOTObj ObjTyInfo - | AOTIFace IFaceTyInfo + = AOTIFace IFaceTyInfo | AOTUnion UnionTyInfo -getPossibleObjTypes' :: TypeMap -> AsObjType -> Map.HashMap G.NamedType ObjTyInfo -getPossibleObjTypes' _ (AOTObj obj) = toObjMap [obj] -getPossibleObjTypes' tyMap (AOTIFace i) = toObjMap $ mapMaybe previewImplTypeM $ Map.elems tyMap - where - previewImplTypeM = \case - TIObj objTyInfo -> bool Nothing (Just objTyInfo) $ - _ifName i `elem` _otiImplIFaces objTyInfo - _ -> Nothing -getPossibleObjTypes' tyMap (AOTUnion u) = toObjMap $ mapMaybe (extrObjTyInfoM tyMap) $ Set.toList $ _utiMemberTypes u +getPossibleObjTypes :: TypeMap -> AsObjType -> Map.HashMap G.NamedType ObjTyInfo +getPossibleObjTypes tyMap = \case + (AOTIFace i) -> + toObjMap $ mapMaybe (extrObjTyInfoM tyMap) $ Set.toList $ _ifMemberTypes i + (AOTUnion u) -> + toObjMap $ mapMaybe (extrObjTyInfoM tyMap) $ Set.toList $ _utiMemberTypes u + -- toObjMap $ mapMaybe previewImplTypeM $ Map.elems tyMap + -- where + -- previewImplTypeM = \case + -- TIObj objTyInfo -> bool Nothing (Just objTyInfo) $ + -- _ifName i `elem` _otiImplIFaces objTyInfo + -- _ -> Nothing + toObjMap :: [ObjTyInfo] -> Map.HashMap G.NamedType ObjTyInfo toObjMap objs = foldr (\o -> Map.insert (_otiName o) o) Map.empty objs @@ -487,7 +492,7 @@ showSPTxt :: SchemaPath -> Text showSPTxt p = showSPTxt' p <> showSP p validateIFace :: MonadError Text f => IFaceTyInfo -> f () -validateIFace (IFaceTyInfo _ n flds) = +validateIFace (IFaceTyInfo _ n flds _) = when (isFldListEmpty flds) $ throwError $ "List of fields cannot be empty for interface " <> showNamedTy n validateObj :: TypeMap -> ObjTyInfo -> Either Text () @@ -631,20 +636,31 @@ mkTyInfoMap :: [TypeInfo] -> TypeMap mkTyInfoMap tyInfos = Map.fromList [(getNamedTy tyInfo, tyInfo) | tyInfo <- tyInfos] -fromTyDef :: G.TypeDefinition -> TypeLoc -> TypeInfo -fromTyDef tyDef loc = case tyDef of +fromTyDef :: InterfaceImplementations -> TypeLoc -> G.TypeDefinition -> TypeInfo +fromTyDef interfaceImplementations loc tyDef = case tyDef of G.TypeDefinitionScalar t -> TIScalar $ fromScalarTyDef t loc G.TypeDefinitionObject t -> TIObj $ fromObjTyDef t loc - G.TypeDefinitionInterface t -> TIIFace $ fromIFaceDef t loc + G.TypeDefinitionInterface t -> TIIFace $ fromIFaceDef interfaceImplementations t loc G.TypeDefinitionUnion t -> TIUnion $ fromUnionTyDef t G.TypeDefinitionEnum t -> TIEnum $ fromEnumTyDef t loc G.TypeDefinitionInputObject t -> TIInpObj $ fromInpObjTyDef t loc +type InterfaceImplementations = Map.HashMap G.NamedType MemberTypes + fromSchemaDoc :: G.SchemaDocument -> TypeLoc -> Either Text TypeMap fromSchemaDoc (G.SchemaDocument tyDefs) loc = do - let tyMap = mkTyInfoMap $ map (flip fromTyDef loc) tyDefs + let tyMap = mkTyInfoMap $ map (fromTyDef interfaceImplementations loc) tyDefs validateTypeMap tyMap return tyMap + where + interfaceImplementations :: InterfaceImplementations + interfaceImplementations = + foldr (Map.unionWith (<>)) mempty $ flip mapMaybe tyDefs $ \case + G.TypeDefinitionObject objectDefinition -> + Just $ Map.fromList $ zip + (G._otdImplementsInterfaces objectDefinition) + (repeat $ Set.singleton $ G.NamedType $ G._otdName objectDefinition) + _ -> Nothing validateTypeMap :: TypeMap -> Either Text () validateTypeMap tyMap = mapM_ validateTy $ Map.elems tyMap @@ -654,9 +670,6 @@ validateTypeMap tyMap = mapM_ validateTy $ Map.elems tyMap validateTy (TIIFace i) = validateIFace i validateTy _ = return () -fromTyDefQ :: G.TypeDefinition -> TypeLoc -> TH.Q TH.Exp -fromTyDefQ tyDef loc = TH.lift $ fromTyDef tyDef loc - fromSchemaDocQ :: G.SchemaDocument -> TypeLoc -> TH.Q TH.Exp fromSchemaDocQ sd loc = case fromSchemaDoc sd loc of Left e -> fail $ T.unpack e @@ -697,63 +710,21 @@ defDirectivesMap = mapFromL _diName defaultDirectives data FragDef = FragDef { _fdName :: !G.Name - , _fdTyInfo :: !ObjTyInfo + , _fdTyInfo :: !FragmentTypeInfo , _fdSelSet :: !G.SelectionSet } deriving (Show, Eq) +data FragmentTypeInfo + = FragmentTyObject !ObjTyInfo + | FragmentTyInterface !IFaceTyInfo + | FragmentTyUnion !UnionTyInfo + deriving (Show, Eq) + type FragDefMap = Map.HashMap G.Name FragDef type AnnVarVals = Map.HashMap G.Variable AnnInpVal -data AnnInpVal - = AnnInpVal - { _aivType :: !G.GType - , _aivVariable :: !(Maybe G.Variable) - , _aivValue :: !AnnGValue - } deriving (Show, Eq) - -type AnnGObject = OMap.InsOrdHashMap G.Name AnnInpVal - --- | See 'EnumValuesInfo' for information about what these cases mean. -data AnnGEnumValue - = AGESynthetic !(Maybe G.EnumValue) - | AGEReference !RQL.EnumReference !(Maybe RQL.EnumValue) - deriving (Show, Eq) - -data AnnGValue - = AGScalar !PGScalarType !(Maybe PGScalarValue) - | AGEnum !G.NamedType !AnnGEnumValue - | AGObject !G.NamedType !(Maybe AnnGObject) - | AGArray !G.ListType !(Maybe [AnnInpVal]) - deriving (Show, Eq) - -$(J.deriveToJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True} - ''AnnInpVal - ) - -instance J.ToJSON AnnGValue where - -- toJSON (AGScalar ty valM) = - toJSON = const J.Null - -- J. - -- J.toJSON [J.toJSON ty, J.toJSON valM] - -hasNullVal :: AnnGValue -> Bool -hasNullVal = \case - AGScalar _ Nothing -> True - AGEnum _ (AGESynthetic Nothing) -> True - AGEnum _ (AGEReference _ Nothing) -> True - AGObject _ Nothing -> True - AGArray _ Nothing -> True - _ -> False - -getAnnInpValKind :: AnnGValue -> Text -getAnnInpValKind = \case - AGScalar _ _ -> "scalar" - AGEnum _ _ -> "enum" - AGObject _ _ -> "object" - AGArray _ _ -> "array" - stripTypenames :: [G.ExecutableDefinition] -> [G.ExecutableDefinition] stripTypenames = map filterExecDef where @@ -826,6 +797,10 @@ instance (MonadReusability m) => MonadReusability (ReaderT r m) where recordVariableUse a b = lift $ recordVariableUse a b markNotReusable = lift markNotReusable +instance (MonadReusability m) => MonadReusability (StateT s m) where + recordVariableUse a b = lift $ recordVariableUse a b + markNotReusable = lift markNotReusable + newtype ReusabilityT m a = ReusabilityT { unReusabilityT :: StateT QueryReusability m a } deriving (Functor, Applicative, Monad, MonadError e, MonadReader r, MonadIO) From 94153cf78610132dabf9f2dd67da039d1fa9a24c Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi Date: Fri, 8 May 2020 16:51:54 +0530 Subject: [PATCH 05/29] use the new normal form everywhere --- server/cabal.project | 4 +- server/src-lib/Hasura/GraphQL/Execute.hs | 39 ++++++----- .../src-lib/Hasura/GraphQL/Execute/Query.hs | 12 ++-- server/src-lib/Hasura/GraphQL/Explain.hs | 11 ++-- server/src-lib/Hasura/GraphQL/NormalForm.hs | 66 +++++++++++++++++++ .../src-lib/Hasura/GraphQL/Resolve/Action.hs | 17 +++-- .../src-lib/Hasura/GraphQL/Resolve/Context.hs | 10 +-- .../src-lib/Hasura/GraphQL/Resolve/Insert.hs | 8 ++- .../Hasura/GraphQL/Resolve/Introspect.hs | 21 +++--- .../Hasura/GraphQL/Resolve/Mutation.hs | 17 +++-- .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 41 +++++++----- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 15 ++++- .../Hasura/GraphQL/Transport/HTTP/Protocol.hs | 2 +- .../Hasura/GraphQL/Transport/WebSocket.hs | 2 +- server/src-lib/Hasura/GraphQL/Validate.hs | 37 +++++------ .../Hasura/GraphQL/Validate/SelectionSet.hs | 1 + 16 files changed, 197 insertions(+), 106 deletions(-) diff --git a/server/cabal.project b/server/cabal.project index 270808e74ac39..0191292519413 100644 --- a/server/cabal.project +++ b/server/cabal.project @@ -33,8 +33,8 @@ source-repository-package source-repository-package type: git - location: https://github.com/hasura/graphql-parser-hs.git - tag: 623ad78aa46e7ba2ef1aa58134ad6136b0a85071 + location: https://github.com/0x777/graphql-parser-hs.git + tag: 4d4e91296011360a21df888020275269cf0ca193 source-repository-package type: git diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index 6fe8fd2fd07c2..a3691bfaecf33 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -56,6 +56,7 @@ import qualified Hasura.GraphQL.Execute.Query as EQ import qualified Hasura.GraphQL.Resolve as GR import qualified Hasura.GraphQL.Validate as VQ import qualified Hasura.GraphQL.Validate.Types as VT +import qualified Hasura.GraphQL.Validate.SelectionSet as VQ import qualified Hasura.Logging as L import qualified Hasura.Server.Telemetry.Counters as Telem @@ -66,7 +67,7 @@ import qualified Hasura.Server.Telemetry.Counters as Telem -- intermediate passes data GQExecPlan a = GExPHasura !a - | GExPRemote !RemoteSchemaInfo !G.TypedOperationDefinition + | GExPRemote !RemoteSchemaInfo !G.TypedOperationDefinition !VQ.RootSelectionSet deriving (Functor, Foldable, Traversable) -- | Execution context @@ -116,7 +117,7 @@ gatherTypeLocs gCtx nodes = in maybe qr (Map.union qr) mr -- This is for when the graphql query is validated -type ExecPlanPartial = GQExecPlan (GCtx, VQ.RootSelSet) +type ExecPlanPartial = GQExecPlan (GCtx, VQ.RootSelectionSet) getExecPlanPartial :: (MonadReusability m, MonadError QErr m) @@ -145,8 +146,9 @@ getExecPlanPartial userInfo sc enableAL req = do VT.TLHasuraType -> do rootSelSet <- runReaderT (VQ.validateGQ queryParts) gCtx return $ GExPHasura (gCtx, rootSelSet) - VT.TLRemoteType _ rsi -> - return $ GExPRemote rsi opDef + VT.TLRemoteType _ rsi -> do + rootSelSet <- runReaderT (VQ.validateGQ queryParts) gCtx + return $ GExPRemote rsi opDef rootSelSet VT.TLCustom -> throw500 "unexpected custom type for top level field" where @@ -217,11 +219,13 @@ getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx (tx, respHeaders) <- getMutOp gCtx sqlGenCtx userInfo httpManager reqHeaders selSet pure $ ExOpMutation respHeaders tx VQ.RQuery selSet -> do - (queryTx, plan, genSql) <- getQueryOp gCtx sqlGenCtx userInfo queryReusability (allowQueryActionExecuter httpManager reqHeaders) selSet + (queryTx, plan, genSql) <- getQueryOp gCtx sqlGenCtx userInfo queryReusability + (allowQueryActionExecuter httpManager reqHeaders) selSet traverse_ (addPlanToCache . EP.RPQuery) plan return $ ExOpQuery queryTx (Just genSql) - VQ.RSubscription fld -> do - (lqOp, plan) <- getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability (restrictActionExecuter "query actions cannot be run as a subscription") fld + VQ.RSubscription alias fld -> do + (lqOp, plan) <- getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability + (restrictActionExecuter "query actions cannot be run as a subscription") alias fld traverse_ (addPlanToCache . EP.RPSubs) plan return $ ExOpSubs lqOp @@ -265,7 +269,7 @@ getQueryOp -> UserInfo -> QueryReusability -> QueryActionExecuter - -> VQ.SelSet + -> VQ.ObjectSelectionSet -> m (LazyRespTx, Maybe EQ.ReusableQueryPlan, EQ.GeneratedSqlMap) getQueryOp gCtx sqlGenCtx userInfo queryReusability actionExecuter selSet = runE gCtx sqlGenCtx userInfo $ EQ.convertQuerySelSet queryReusability selSet actionExecuter @@ -284,14 +288,13 @@ resolveMutSelSet , Has [N.Header] r , MonadIO m ) - => VQ.SelSet + => VQ.ObjectSelectionSet -> m (LazyRespTx, N.ResponseHeaders) resolveMutSelSet fields = do - aliasedTxs <- forM (toList fields) $ \fld -> do - fldRespTx <- case VQ._fName fld of + aliasedTxs <- traverseObjectSelectionSet fields $ \fld -> do + case VQ._fName fld of "__typename" -> return (return $ encJFromJValue mutationRootNamedType, []) _ -> evalReusabilityT $ GR.mutFldToTx fld - return (G.unName $ G.unAlias $ VQ._fAlias fld, fldRespTx) -- combines all transactions into a single transaction return (liftTx $ toSingleTx aliasedTxs, concatMap (snd . snd) aliasedTxs) @@ -312,7 +315,7 @@ getMutOp -> UserInfo -> HTTP.Manager -> [N.Header] - -> VQ.SelSet + -> VQ.ObjectSelectionSet -> m (LazyRespTx, N.ResponseHeaders) getMutOp ctx sqlGenCtx userInfo manager reqHeaders selSet = peelReaderT $ resolveMutSelSet selSet @@ -344,10 +347,11 @@ getSubsOpM ) => PGExecCtx -> QueryReusability + -> G.Alias -> VQ.Field -> QueryActionExecuter -> m (EL.LiveQueryPlan, Maybe EL.ReusableLiveQueryPlan) -getSubsOpM pgExecCtx initialReusability fld actionExecuter = +getSubsOpM pgExecCtx initialReusability alias fld actionExecuter = case VQ._fName fld of "__typename" -> throwVE "you cannot create a subscription on '__typename' field" @@ -355,7 +359,7 @@ getSubsOpM pgExecCtx initialReusability fld actionExecuter = (astUnresolved, finalReusability) <- runReusabilityTWith initialReusability $ GR.queryFldToPGAST fld actionExecuter let varTypes = finalReusability ^? _Reusable - EL.buildLiveQueryPlan pgExecCtx (VQ._fAlias fld) astUnresolved varTypes + EL.buildLiveQueryPlan pgExecCtx alias astUnresolved varTypes getSubsOp :: ( MonadError QErr m @@ -368,10 +372,11 @@ getSubsOp -> UserInfo -> QueryReusability -> QueryActionExecuter + -> G.Alias -> VQ.Field -> m (EL.LiveQueryPlan, Maybe EL.ReusableLiveQueryPlan) -getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability actionExecuter fld = - runE gCtx sqlGenCtx userInfo $ getSubsOpM pgExecCtx queryReusability fld actionExecuter +getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability actionExecuter alias fld = + runE gCtx sqlGenCtx userInfo $ getSubsOpM pgExecCtx queryReusability alias fld actionExecuter execRemoteGQ :: ( HasVersion diff --git a/server/src-lib/Hasura/GraphQL/Execute/Query.hs b/server/src-lib/Hasura/GraphQL/Execute/Query.hs index 49098984c2e88..44be5135ce9e1 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Query.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Query.hs @@ -21,7 +21,7 @@ import Data.Has import qualified Hasura.GraphQL.Resolve as R import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH import qualified Hasura.GraphQL.Validate as GV -import qualified Hasura.GraphQL.Validate.Field as V +import qualified Hasura.GraphQL.Validate.SelectionSet as V import qualified Hasura.SQL.DML as S import Hasura.EncJSON @@ -191,14 +191,14 @@ convertQuerySelSet , MonadIO m ) => QueryReusability - -> V.SelSet + -> V.ObjectSelectionSet -> QueryActionExecuter -> m (LazyRespTx, Maybe ReusableQueryPlan, GeneratedSqlMap) -convertQuerySelSet initialReusability fields actionRunner = do +convertQuerySelSet initialReusability selSet actionRunner = do usrVars <- asks (_uiSession . getter) (fldPlans, finalReusability) <- runReusabilityTWith initialReusability $ - forM (toList fields) $ \fld -> do - fldPlan <- case V._fName fld of + fmap (map (\(a, b) -> (G.Alias $ G.Name a, b))) $ V.traverseObjectSelectionSet selSet $ \fld -> do + case V._fName fld of "__type" -> fldPlanFromJ <$> R.typeR fld "__schema" -> fldPlanFromJ <$> R.schemaR fld "__typename" -> pure $ fldPlanFromJ queryRootNamedType @@ -207,7 +207,7 @@ convertQuerySelSet initialReusability fields actionRunner = do (q, PlanningSt _ vars prepped) <- flip runStateT initPlanningSt $ R.traverseQueryRootFldAST prepareWithPlan unresolvedAst pure . RFPPostgres $ PGPlan (R.toPGQuery q) vars prepped - pure (V._fAlias fld, fldPlan) + -- pure (V._fAlias fld, fldPlan) let varTypes = finalReusability ^? _Reusable reusablePlan = ReusableQueryPlan <$> varTypes <*> pure fldPlans (tx, sql) <- mkCurPlanTx usrVars fldPlans diff --git a/server/src-lib/Hasura/GraphQL/Explain.hs b/server/src-lib/Hasura/GraphQL/Explain.hs index 4de22a50bf841..9d93c84cb2e15 100644 --- a/server/src-lib/Hasura/GraphQL/Explain.hs +++ b/server/src-lib/Hasura/GraphQL/Explain.hs @@ -27,6 +27,7 @@ import qualified Hasura.GraphQL.Execute.LiveQuery as E import qualified Hasura.GraphQL.Resolve as RS import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH import qualified Hasura.GraphQL.Validate as GV +import qualified Hasura.GraphQL.Validate.SelectionSet as GV import qualified Hasura.SQL.DML as S data GQLExplain @@ -129,16 +130,16 @@ explainGQLQuery pgExecCtx sc sqlGenCtx enableAL actionExecuter (GQLExplain query (gCtx, rootSelSet) <- case execPlan of E.GExPHasura (gCtx, rootSelSet) -> return (gCtx, rootSelSet) - E.GExPRemote _ _ -> + E.GExPRemote _ _ _ -> throw400 InvalidParams "only hasura queries can be explained" case rootSelSet of GV.RQuery selSet -> - runInTx $ encJFromJValue <$> traverse (explainField userInfo gCtx sqlGenCtx actionExecuter) - (toList selSet) + runInTx $ encJFromJValue <$> GV.traverseObjectSelectionSet selSet (explainField userInfo gCtx sqlGenCtx actionExecuter) GV.RMutation _ -> throw400 InvalidParams "only queries can be explained" - GV.RSubscription rootField -> do - (plan, _) <- E.getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability actionExecuter rootField + GV.RSubscription alias rootField -> do + (plan, _) <- E.getSubsOp pgExecCtx gCtx sqlGenCtx userInfo + queryReusability actionExecuter alias rootField runInTx $ encJFromJValue <$> E.explainLiveQueryPlan plan where sessionVariables = mkSessionVariablesText $ maybe [] Map.toList userVarsRaw diff --git a/server/src-lib/Hasura/GraphQL/NormalForm.hs b/server/src-lib/Hasura/GraphQL/NormalForm.hs index af7df978c4aa3..c07f6319d082e 100644 --- a/server/src-lib/Hasura/GraphQL/NormalForm.hs +++ b/server/src-lib/Hasura/GraphQL/NormalForm.hs @@ -6,6 +6,8 @@ module Hasura.GraphQL.NormalForm , DenormalizedSelectionSet , DenormalizedField , SelectionSet(..) + , RootSelectionSet(..) + , toGraphQLOperation , ArgsMap , Field(..) , Typename(..) @@ -31,6 +33,9 @@ module Hasura.GraphQL.NormalForm , AnnGEnumValue(..) , hasNullVal , getAnnInpValKind + + , toGraphQLField + , toGraphQLSelectionSet ) where import Hasura.Prelude @@ -91,6 +96,8 @@ data ScopedSelectionSet f -- ^ SelectionSets of individual member types } deriving (Show, Eq, Generic) + + emptyScopedSelectionSet :: ScopedSelectionSet f emptyScopedSelectionSet = ScopedSelectionSet (AliasedFields mempty) mempty @@ -99,6 +106,36 @@ type InterfaceSelectionSet = ScopedSelectionSet Field type UnionSelectionSet = ScopedSelectionSet Typename +data RootSelectionSet + = RQuery !ObjectSelectionSet + | RMutation !ObjectSelectionSet + | RSubscription !G.Alias !Field + deriving (Show, Eq) + +toGraphQLOperation :: RootSelectionSet -> G.ExecutableDefinition +toGraphQLOperation = \case + RQuery selectionSet -> + mkExecutableDefinition G.OperationTypeQuery $ + toGraphQLSelectionSet $ SelectionSetObject selectionSet + RMutation selectionSet -> + mkExecutableDefinition G.OperationTypeQuery $ + toGraphQLSelectionSet $ SelectionSetObject selectionSet + RSubscription alias field -> + mkExecutableDefinition G.OperationTypeSubscription $ + toGraphQLSelectionSet $ SelectionSetObject $ ObjectSelectionSet $ + AliasedFields $ OMap.singleton alias field + where + mkExecutableDefinition operationType selectionSet = + G.ExecutableDefinitionOperation $ G.OperationDefinitionTyped $ + G.TypedOperationDefinition + { G._todName = Nothing -- TODO, store the name too? + , G._todDirectives = [] + , G._todType = operationType + , G._todVariableDefinitions = [] + , G._todSelectionSet = selectionSet + } + + data SelectionSet = SelectionSetObject !ObjectSelectionSet | SelectionSetUnion !UnionSelectionSet @@ -138,6 +175,35 @@ data Field , _fSelSet :: !SelectionSet } deriving (Eq, Show) +toGraphQLField :: G.Alias -> Field -> G.Field +toGraphQLField alias Field{..} = + G.Field + { G._fName = _fName + , G._fArguments = [] -- TODO + , G._fDirectives = [] + , G._fAlias = Just alias + , G._fSelectionSet = toGraphQLSelectionSet _fSelSet + } + +toGraphQLSelectionSet :: SelectionSet -> G.SelectionSet +toGraphQLSelectionSet = \case + SelectionSetObject selectionSet -> fromSelectionSet selectionSet + SelectionSetInterface selectionSet -> fromScopedSelectionSet selectionSet + SelectionSetUnion selectionSet -> fromScopedSelectionSet selectionSet + SelectionSetNone -> mempty + where + fromAliasedFields :: (IsField f) => AliasedFields f -> G.SelectionSet + fromAliasedFields = + map (G.SelectionField . uncurry toGraphQLField) . + OMap.toList . fmap toField . unAliasedFields + fromSelectionSet = + fromAliasedFields . unObjectSelectionSet + toInlineSelection typeName = + G.SelectionInlineFragment . G.InlineFragment (Just typeName) mempty . + fromSelectionSet + fromScopedSelectionSet (ScopedSelectionSet base specific) = + map (uncurry toInlineSelection) (Map.toList specific) <> fromAliasedFields base + -- $(J.deriveToJSON (J.aesonDrop 2 J.camelCase){J.omitNothingFields=True} -- ''Field -- ) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Action.hs b/server/src-lib/Hasura/GraphQL/Resolve/Action.hs index 9fca24d44dac9..a4111e844abcd 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Action.hs @@ -42,7 +42,7 @@ import Hasura.EncJSON import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Resolve.Select (processTableSelectionSet) -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types import Hasura.HTTP import Hasura.RQL.DDL.Headers (HeaderConf, makeHeadersFromConf, toHeadersConf) @@ -166,9 +166,10 @@ resolveActionMutationSync field executionContext sessionVariables = do forwardClientHeaders resolvedWebhook handlerPayload let webhookResponseExpression = RS.AEInput $ UVSQL $ toTxtValue $ WithScalarType PGJSONB $ PGValJSONB $ Q.JSONB $ J.toJSON webhookRes + selSet <- asObjectSelectionSet $ _fSelSet field selectAstUnresolved <- processOutputSelectionSet webhookResponseExpression outputType definitionList - (_fType field) $ _fSelSet field + (_fType field) selSet astResolved <- RS.traverseAnnSimpleSel resolveValTxt selectAstUnresolved let jsonAggType = mkJsonAggSelect outputType return $ (,respHeaders) $ asSingleRowJsonResp (RS.selectQuerySQL jsonAggType astResolved) [] @@ -218,9 +219,10 @@ resolveActionQuery field executionContext sessionVariables httpManager reqHeader forwardClientHeaders resolvedWebhook handlerPayload let webhookResponseExpression = RS.AEInput $ UVSQL $ toTxtValue $ WithScalarType PGJSONB $ PGValJSONB $ Q.JSONB $ J.toJSON webhookRes + selSet <- asObjectSelectionSet $ _fSelSet field selectAstUnresolved <- processOutputSelectionSet webhookResponseExpression outputType definitionList - (_fType field) $ _fSelSet field + (_fType field) selSet return selectAstUnresolved where ActionExecutionContext actionName outputType outputFields definitionList resolvedWebhook confHeaders @@ -293,7 +295,9 @@ resolveAsyncActionQuery userInfo selectOpCtx field = do actionId <- withArg (_fArguments field) "id" parseActionId stringifyNumerics <- stringifyNum <$> asks getter - annotatedFields <- fmap (map (first FieldName)) $ withSelSet (_fSelSet field) $ \fld -> + selSet <- asObjectSelectionSet $ _fSelSet field + + annotatedFields <- fmap (map (first FieldName)) $ traverseObjectSelectionSet selSet $ \fld -> case _fName fld of "__typename" -> return $ RS.FExp $ G.unName $ G.unNamedType $ _fType field "output" -> do @@ -301,9 +305,10 @@ resolveAsyncActionQuery userInfo selectOpCtx field = do let inputTableArgument = RS.AETableRow $ Just $ Iden "response_payload" ActionSelectOpContext outputType definitionList = selectOpCtx jsonAggSelect = mkJsonAggSelect outputType + fldSelSet <- asObjectSelectionSet $ _fSelSet fld (RS.FComputedField . RS.CFSTable jsonAggSelect) <$> processOutputSelectionSet inputTableArgument outputType - definitionList (_fType fld) (_fSelSet fld) + definitionList (_fType fld) fldSelSet -- The metadata columns "id" -> return $ mkAnnFldFromPGCol "id" PGUUID @@ -561,7 +566,7 @@ processOutputSelectionSet => RS.ArgumentExp UnresolvedVal -> GraphQLType -> [(PGCol, PGScalarType)] - -> G.NamedType -> SelSet -> m GRS.AnnSimpleSelect + -> G.NamedType -> ObjectSelectionSet -> m GRS.AnnSimpleSelect processOutputSelectionSet tableRowInput actionOutputType definitionList fldTy flds = do stringifyNumerics <- stringifyNum <$> asks getter annotatedFields <- processTableSelectionSet fldTy flds diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs index 1d1974eae4e28..0178184646e0d 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs @@ -23,7 +23,7 @@ module Hasura.GraphQL.Resolve.Context , txtConverter - , withSelSet + , traverseObjectSelectionSet , fieldAsPath , resolvePGCol , module Hasura.GraphQL.Utils @@ -40,7 +40,7 @@ import qualified Language.GraphQL.Draft.Syntax as G import Hasura.GraphQL.Resolve.Types import Hasura.GraphQL.Utils -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Internal (currentSession, sessVarFromCurrentSetting) import Hasura.RQL.Types @@ -139,12 +139,6 @@ prepareColVal (WithScalarType scalarType colVal) = do txtConverter :: Applicative f => AnnPGVal -> f S.SQLExp txtConverter (AnnPGVal _ _ scalarValue) = pure $ toTxtValue scalarValue -withSelSet :: (Monad m) => SelSet -> (Field -> m a) -> m [(Text, a)] -withSelSet selSet f = - forM (toList selSet) $ \fld -> do - res <- f fld - return (G.unName $ G.unAlias $ _fAlias fld, res) - fieldAsPath :: (MonadError QErr m) => Field -> m a -> m a fieldAsPath = nameAsPath . _fName diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs index 13e83c5ec5a52..2e13aee89b899 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs @@ -30,7 +30,7 @@ import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Resolve.Mutation import Hasura.GraphQL.Resolve.Select -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Insert (insertOrUpdateCheckExpr) import Hasura.RQL.DML.Internal (convAnnBoolExpPartialSQL, convPartialSQLExp, @@ -475,7 +475,8 @@ convertInsert -> Field -- the mutation field -> m RespTx convertInsert role tn fld = prefixErrPath fld $ do - mutOutputUnres <- RR.MOutMultirowFields <$> resolveMutationFields (_fType fld) (_fSelSet fld) + selSet <- asObjectSelectionSet $ _fSelSet fld + mutOutputUnres <- RR.MOutMultirowFields <$> resolveMutationFields (_fType fld) selSet mutOutputRes <- RR.traverseMutationOutput resolveValTxt mutOutputUnres annVals <- withArg arguments "objects" asArray -- if insert input objects is empty array then @@ -510,7 +511,8 @@ convertInsertOne -> Field -- the mutation field -> m RespTx convertInsertOne role qt field = prefixErrPath field $ do - tableSelFields <- processTableSelectionSet (_fType field) $ _fSelSet field + selSet <- asObjectSelectionSet $ _fSelSet field + tableSelFields <- processTableSelectionSet (_fType field) selSet let mutationOutputUnresolved = RR.MOutSinglerowObject tableSelFields mutationOutputResolved <- RR.traverseMutationOutput resolveValTxt mutationOutputUnresolved annInputObj <- withArg arguments "object" asObject diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs b/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs index 4e851bacd2d3d..2102f93bdd869 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs @@ -16,7 +16,7 @@ import Hasura.GraphQL.Context import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Validate.Context -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.InputValue import Hasura.GraphQL.Validate.Types import Hasura.RQL.Types @@ -36,14 +36,15 @@ instance J.ToJSON TypeKind where toJSON = J.toJSON . T.pack . drop 2 . show withSubFields - :: (Monad m) - => SelSet + :: (MonadError QErr m) + => SelectionSet -> (Field -> m J.Value) -> m J.Object -withSubFields selSet fn = - fmap Map.fromList $ forM (toList selSet) $ \fld -> do - val <- fn fld - return (G.unName $ G.unAlias $ _fAlias fld, val) +withSubFields selSet fn = do + objectSelectionSet <- asObjectSelectionSet selSet + fmap Map.fromList $ traverseObjectSelectionSet objectSelectionSet fn + -- val <- fn fld + -- return (G.unName $ G.unAlias $ _fAlias fld, val) namedTyToTxt :: G.NamedType -> Text namedTyToTxt = G.unName . G.unNamedType @@ -56,7 +57,7 @@ retJT = pure . J.toJSON -- 4.5.2.1 scalarR - :: (Monad m) + :: (MonadError QErr m) => ScalarTyInfo -> Field -> m J.Object @@ -156,7 +157,7 @@ ifaceR' i@(IFaceTyInfo descM n flds implementations) fld = -- 4.5.2.5 enumTypeR - :: ( Monad m ) + :: (MonadError QErr m) => EnumTyInfo -> Field -> m J.Object @@ -277,7 +278,7 @@ inputValueR fld (InpValInfo descM n defM ty) = -- 4.5.5 enumValueR - :: (Monad m) + :: (MonadError QErr m) => Field -> EnumValInfo -> m J.Object enumValueR fld (EnumValInfo descM enumVal isDeprecated) = withSubFields (_fSelSet fld) $ \subFld -> diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs index dec9a37783ab8..37862086bcecc 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs @@ -33,7 +33,7 @@ import Hasura.GraphQL.Resolve.BoolExp import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Resolve.Select (processTableSelectionSet) -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Internal (currentSession, sessVarFromCurrentSetting) import Hasura.RQL.Types @@ -44,13 +44,14 @@ resolveMutationFields :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => G.NamedType -> SelSet -> m (RR.MutFldsG UnresolvedVal) + => G.NamedType -> ObjectSelectionSet -> m (RR.MutFldsG UnresolvedVal) resolveMutationFields ty selSet = fmap (map (first FieldName)) $ - withSelSet selSet $ \fld -> case _fName fld of + traverseObjectSelectionSet selSet $ \fld -> case _fName fld of "__typename" -> return $ RR.MExp $ G.unName $ G.unNamedType ty "affected_rows" -> return RR.MCount "returning" -> do - annFlds <- processTableSelectionSet (_fType fld) $ _fSelSet fld + annFlds <- asObjectSelectionSet (_fSelSet fld) + >>= processTableSelectionSet (_fType fld) annFldsResolved <- traverse (traverse (RS.traverseAnnFld convertPGValueToTextValue)) annFlds return $ RR.MRet annFldsResolved @@ -321,8 +322,9 @@ mutationFieldsResolver , Has OrdByCtx r, Has SQLGenCtx r ) => Field -> m (RR.MutationOutputG UnresolvedVal) -mutationFieldsResolver field = - RR.MOutMultirowFields <$> resolveMutationFields (_fType field) (_fSelSet field) +mutationFieldsResolver field = do + asObjectSelectionSet (_fSelSet field) >>= \selSet -> + RR.MOutMultirowFields <$> resolveMutationFields (_fType field) selSet tableSelectionAsMutationOutput :: ( MonadReusability m, MonadError QErr m @@ -331,7 +333,8 @@ tableSelectionAsMutationOutput ) => Field -> m (RR.MutationOutputG UnresolvedVal) tableSelectionAsMutationOutput field = - RR.MOutSinglerowObject <$> processTableSelectionSet (_fType field) (_fSelSet field) + asObjectSelectionSet (_fSelSet field) >>= \selSet -> + RR.MOutSinglerowObject <$> processTableSelectionSet (_fType field) selSet -- | build mutation response for empty objects buildEmptyMutResp :: RR.MutationOutput -> EncJSON diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index 5695b4b6fe830..7c5bcc07311f6 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -28,7 +28,7 @@ import Hasura.GraphQL.Resolve.BoolExp import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Schema (isAggFld) -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Internal (onlyPositiveInt) import Hasura.RQL.Types @@ -97,12 +97,11 @@ processTableSelectionSet :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => G.NamedType -> SelSet -> m AnnFlds + => G.NamedType -> ObjectSelectionSet -> m AnnFlds processTableSelectionSet fldTy flds = - forM (toList flds) $ \fld -> do + fmap (map (\(a, b) -> (FieldName a, b))) $ traverseObjectSelectionSet flds $ \fld -> do let fldName = _fName fld - let rqlFldName = FieldName $ G.unName $ G.unAlias $ _fAlias fld - (rqlFldName,) <$> case fldName of + case fldName of "__typename" -> return $ RS.FExp $ G.unName $ G.unNamedType fldTy _ -> do fldInfo <- getFldInfo fldTy fldName @@ -131,15 +130,18 @@ fromAggSelSet :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => PGColGNameMap -> G.NamedType -> SelSet -> m TableAggFlds + => PGColGNameMap -> G.NamedType -> ObjectSelectionSet -> m TableAggFlds fromAggSelSet colGNameMap fldTy selSet = fmap toFields $ - withSelSet selSet $ \f -> do + traverseObjectSelectionSet selSet $ \f -> do let fTy = _fType f - fSelSet = _fSelSet f case _fName f of "__typename" -> return $ RS.TAFExp $ G.unName $ G.unNamedType fldTy - "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet - "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet + "aggregate" -> do + objSelSet <- asObjectSelectionSet $ _fSelSet f + RS.TAFAgg <$> convertAggFld colGNameMap fTy objSelSet + "nodes" -> do + objSelSet <- asObjectSelectionSet $ _fSelSet f + RS.TAFNodes <$> processTableSelectionSet fTy objSelSet G.Name t -> throw500 $ "unexpected field in _agg node: " <> t type TableArgs = RS.TableArgsG UnresolvedVal @@ -187,7 +189,8 @@ fromField -> Field -> m AnnSimpleSelect fromField selFrom colGNameMap permFilter permLimitM fld = fieldAsPath fld $ do tableArgs <- parseTableArgs colGNameMap args - annFlds <- processTableSelectionSet (_fType fld) $ _fSelSet fld + selSet <- asObjectSelectionSet $ _fSelSet fld + annFlds <- processTableSelectionSet (_fType fld) selSet let unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter let tabPerm = RS.TablePerm unresolvedPermFltr permLimitM strfyNum <- stringifyNum <$> asks getter @@ -322,7 +325,8 @@ fromFieldByPKey -> AnnBoolExpPartialSQL -> Field -> m AnnSimpleSel fromFieldByPKey tn colArgMap permFilter fld = fieldAsPath fld $ do boolExp <- pgColValToBoolExp colArgMap $ _fArguments fld - annFlds <- processTableSelectionSet fldTy $ _fSelSet fld + selSet <- asObjectSelectionSet $ _fSelSet fld + annFlds <- processTableSelectionSet fldTy selSet let tabFrom = RS.FromTable tn unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter @@ -384,24 +388,24 @@ toFields = map (first FieldName) convertColFlds :: (MonadError QErr m) - => PGColGNameMap -> G.NamedType -> SelSet -> m RS.ColFlds + => PGColGNameMap -> G.NamedType -> ObjectSelectionSet -> m RS.ColFlds convertColFlds colGNameMap ty selSet = fmap toFields $ - withSelSet selSet $ \fld -> + traverseObjectSelectionSet selSet $ \fld -> case _fName fld of "__typename" -> return $ RS.PCFExp $ G.unName $ G.unNamedType ty n -> (RS.PCFCol . pgiColumn) <$> resolvePGCol colGNameMap n convertAggFld :: (MonadReusability m, MonadError QErr m) - => PGColGNameMap -> G.NamedType -> SelSet -> m RS.AggFlds + => PGColGNameMap -> G.NamedType -> ObjectSelectionSet -> m RS.AggFlds convertAggFld colGNameMap ty selSet = fmap toFields $ - withSelSet selSet $ \fld -> do + traverseObjectSelectionSet selSet $ \fld -> do let fType = _fType fld - fSelSet = _fSelSet fld case _fName fld of "__typename" -> return $ RS.AFExp $ G.unName $ G.unNamedType ty "count" -> RS.AFCount <$> convertCount colGNameMap (_fArguments fld) n -> do + fSelSet <- asObjectSelectionSet $ _fSelSet fld colFlds <- convertColFlds colGNameMap fType fSelSet unless (isAggFld n) $ throwInvalidFld n return $ RS.AFOp $ RS.AggOp (G.unName n) colFlds @@ -422,7 +426,8 @@ fromAggField -> Field -> m AnnAggSel fromAggField selectFrom colGNameMap permFilter permLimit fld = fieldAsPath fld $ do tableArgs <- parseTableArgs colGNameMap args - aggSelFlds <- fromAggSelSet colGNameMap (_fType fld) (_fSelSet fld) + selSet <- asObjectSelectionSet $ _fSelSet fld + aggSelFlds <- fromAggSelSet colGNameMap (_fType fld) selSet let unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter let tabPerm = RS.TablePerm unresolvedPermFltr permLimit diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index eeae95f30bff5..c70d3d26841c9 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -8,6 +8,7 @@ import qualified Network.HTTP.Types as N import Hasura.EncJSON import Hasura.GraphQL.Logging +import Hasura.GraphQL.NormalForm import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.HTTP import Hasura.Prelude @@ -23,6 +24,7 @@ import qualified Hasura.Logging as L import qualified Hasura.Server.Telemetry.Counters as Telem import qualified Language.GraphQL.Draft.Syntax as G import qualified Network.HTTP.Types as HTTP +import qualified Language.GraphQL.Draft.Printer.Text as GP runGQ :: ( HasVersion @@ -46,10 +48,17 @@ runGQ reqId userInfo reqHdrs req = do E.GExPHasura resolvedOp -> do (telemTimeIO, telemQueryType, respHdrs, resp) <- runHasuraGQ reqId req userInfo resolvedOp return (telemCacheHit, Telem.Local, (telemTimeIO, telemQueryType, HttpResponse resp respHdrs)) - E.GExPRemote rsi opDef -> do + E.GExPRemote rsi opDef rootSelSet -> do let telemQueryType | G._todType opDef == G.OperationTypeMutation = Telem.Mutation - | otherwise = Telem.Query - (telemTimeIO, resp) <- E.execRemoteGQ reqId userInfo reqHdrs req rsi opDef + | otherwise = Telem.Query + rewrittenQuery = + GQLReq { _grQuery = GQLQueryText $ GP.renderExecutableDoc $ + G.ExecutableDocument $ pure $ + toGraphQLOperation rootSelSet + , _grVariables = Nothing + , _grOperationName = Nothing + } + (telemTimeIO, resp) <- E.execRemoteGQ reqId userInfo reqHdrs rewrittenQuery rsi opDef return (telemCacheHit, Telem.Remote, (telemTimeIO, telemQueryType, resp)) let telemTimeIO = fromUnits telemTimeIO_DT telemTimeTot = fromUnits telemTimeTot_DT diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP/Protocol.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP/Protocol.hs index c3bd1c5eaeae7..feedbc15093e3 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP/Protocol.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP/Protocol.hs @@ -4,7 +4,7 @@ module Hasura.GraphQL.Transport.HTTP.Protocol , GQLReqUnparsed , GQLReqParsed , toParsed - , GQLQueryText + , GQLQueryText(..) , GQLExecDoc(..) , OperationName(..) , VariableValues diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index f0f5a29cc3bf1..4492aabcdb0ee 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -316,7 +316,7 @@ onStart serverEnv wsConn (StartMsg opId q) = catchAndIgnore $ do case execPlan of E.GExPHasura resolvedOp -> runHasuraGQ timerTot telemCacheHit requestId q userInfo resolvedOp - E.GExPRemote rsi opDef -> + E.GExPRemote rsi opDef rootSelectionSet -> runRemoteGQ timerTot telemCacheHit execCtx requestId userInfo reqHdrs opDef rsi where telemTransport = Telem.HTTP diff --git a/server/src-lib/Hasura/GraphQL/Validate.hs b/server/src-lib/Hasura/GraphQL/Validate.hs index 140fd5143614c..2e10e4a191a2d 100644 --- a/server/src-lib/Hasura/GraphQL/Validate.hs +++ b/server/src-lib/Hasura/GraphQL/Validate.hs @@ -1,8 +1,8 @@ module Hasura.GraphQL.Validate ( validateGQ , showVars - , RootSelSet(..) - , SelSet + , RootSelectionSet(..) + , SelectionSet(..) , Field(..) , getTypedOp , QueryParts(..) @@ -20,14 +20,14 @@ import Hasura.Prelude import Data.Has import qualified Data.HashMap.Strict as Map +import qualified Data.HashMap.Strict.InsOrd.Extended as OMap import qualified Data.HashSet as HS -import qualified Data.Sequence as Seq import qualified Language.GraphQL.Draft.Syntax as G import Hasura.GraphQL.Schema import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.GraphQL.Validate.Context -import Hasura.GraphQL.Validate.Field +import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.InputValue import Hasura.GraphQL.Validate.Types import Hasura.RQL.Types @@ -140,14 +140,9 @@ validateFrag (G.FragmentDefinition n onTy dirs selSet) = do fragmentTypeInfo <- getFragmentTyInfo onTy return $ FragDef n fragmentTypeInfo selSet -data RootSelSet - = RQuery !SelSet - | RMutation !SelSet - | RSubscription !Field - deriving (Show, Eq) - validateGQ - :: (MonadError QErr m, MonadReader GCtx m, MonadReusability m) => QueryParts -> m RootSelSet + :: (MonadError QErr m, MonadReader GCtx m, MonadReusability m) + => QueryParts -> m RootSelectionSet validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do ctx <- ask @@ -163,19 +158,23 @@ validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do -- build a validation ctx let valCtx = ValidationCtx (_gTypes ctx) annVarVals annFragDefs - selSet <- flip runReaderT valCtx $ denormalizeSelectionSet [] opRoot $ + selSet <- flip runReaderT valCtx $ denormalizeObjectSelectionSet valCtx opRoot $ G._todSelectionSet opDef case G._todType opDef of G.OperationTypeQuery -> return $ RQuery selSet G.OperationTypeMutation -> return $ RMutation selSet - G.OperationTypeSubscription -> - case Seq.viewl selSet of - Seq.EmptyL -> throw500 "empty selset for subscription" - fld Seq.:< rst -> do - unless (null rst) $ - throwVE "subscription must select only one top level field" - return $ RSubscription fld + G.OperationTypeSubscription -> do + -- case Seq.viewl selSet of + -- Seq.EmptyL -> throw500 "empty selset for subscription" + -- fld Seq.:< rst -> do + -- unless (null rst) $ + -- throwVE "subscription must select only one top level field" + -- return $ RSubscription fld + case OMap.toList $ unAliasedFields $ unObjectSelectionSet selSet of + [] -> throw500 "empty selset for subscription" + [(alias, field)] -> return $ RSubscription alias field + _ -> throwVE "subscription must select only one top level field" isQueryInAllowlist :: GQLExecDoc -> HS.HashSet GQLQuery -> Bool isQueryInAllowlist q = HS.member gqlQuery diff --git a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs index b653e55012c37..524586bc6a430 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs @@ -9,6 +9,7 @@ module Hasura.GraphQL.Validate.SelectionSet , traverseObjectSelectionSet , InterfaceSelectionSet , UnionSelectionSet + , RootSelectionSet(..) -- , denormalizeSelectionSet , denormalizeObjectSelectionSet , asObjectSelectionSet From f1955aa2859b8b7d4f5db9ebc13a363e0d0032b3 Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi Date: Fri, 8 May 2020 19:36:07 +0530 Subject: [PATCH 06/29] use 'normalize' over 'denormalize' in type names --- server/src-lib/Hasura/GraphQL/NormalForm.hs | 14 +- server/src-lib/Hasura/GraphQL/Validate.hs | 8 +- .../Hasura/GraphQL/Validate/SelectionSet.hs | 215 ++++++++---------- 3 files changed, 108 insertions(+), 129 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/NormalForm.hs b/server/src-lib/Hasura/GraphQL/NormalForm.hs index c07f6319d082e..3436b2d2fd4e6 100644 --- a/server/src-lib/Hasura/GraphQL/NormalForm.hs +++ b/server/src-lib/Hasura/GraphQL/NormalForm.hs @@ -2,9 +2,9 @@ {-# LANGUAGE RecordWildCards #-} module Hasura.GraphQL.NormalForm ( Selection(..) - , DenormalizedSelection - , DenormalizedSelectionSet - , DenormalizedField + , NormalizedSelection + , NormalizedSelectionSet + , NormalizedField , SelectionSet(..) , RootSelectionSet(..) , toGraphQLOperation @@ -59,13 +59,13 @@ data Selection f s deriving (Show, Eq) -- | What a processed G.SelectionSet should look like -type family DenormalizedSelectionSet a = s | s -> a +type family NormalizedSelectionSet a = s | s -> a -- | What a processed G.Field should look like -type family DenormalizedField a +type family NormalizedField a -type DenormalizedSelection a - = Selection (DenormalizedField a) (DenormalizedSelectionSet a) +type NormalizedSelection a + = Selection (NormalizedField a) (NormalizedSelectionSet a) -- | Ordered fields newtype AliasedFields f diff --git a/server/src-lib/Hasura/GraphQL/Validate.hs b/server/src-lib/Hasura/GraphQL/Validate.hs index 2e10e4a191a2d..69c8fda11b2df 100644 --- a/server/src-lib/Hasura/GraphQL/Validate.hs +++ b/server/src-lib/Hasura/GraphQL/Validate.hs @@ -158,19 +158,13 @@ validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do -- build a validation ctx let valCtx = ValidationCtx (_gTypes ctx) annVarVals annFragDefs - selSet <- flip runReaderT valCtx $ denormalizeObjectSelectionSet valCtx opRoot $ + selSet <- flip runReaderT valCtx $ parseObjectSelectionSet valCtx opRoot $ G._todSelectionSet opDef case G._todType opDef of G.OperationTypeQuery -> return $ RQuery selSet G.OperationTypeMutation -> return $ RMutation selSet G.OperationTypeSubscription -> do - -- case Seq.viewl selSet of - -- Seq.EmptyL -> throw500 "empty selset for subscription" - -- fld Seq.:< rst -> do - -- unless (null rst) $ - -- throwVE "subscription must select only one top level field" - -- return $ RSubscription fld case OMap.toList $ unAliasedFields $ unObjectSelectionSet selSet of [] -> throw500 "empty selset for subscription" [(alias, field)] -> return $ RSubscription alias field diff --git a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs index 524586bc6a430..c7831b3a2411f 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs @@ -10,8 +10,7 @@ module Hasura.GraphQL.Validate.SelectionSet , InterfaceSelectionSet , UnionSelectionSet , RootSelectionSet(..) - -- , denormalizeSelectionSet - , denormalizeObjectSelectionSet + , parseObjectSelectionSet , asObjectSelectionSet ) where @@ -37,7 +36,10 @@ class HasSelectionSet a where getTypename :: a -> G.NamedType getMemberTypes :: a -> Set.HashSet G.NamedType - denormalizeField_ + fieldToSelectionSet + :: G.Alias -> NormalizedField a -> NormalizedSelectionSet a + + parseField_ :: ( MonadReader ValidationCtx m , MonadError QErr m , MonadReusability m @@ -45,15 +47,15 @@ class HasSelectionSet a where ) => a -> G.Field - -> m (Maybe (DenormalizedField a)) + -> m (Maybe (NormalizedField a)) - mergeSelections + mergeNormalizedSelectionSets :: ( MonadReader ValidationCtx m , MonadError QErr m , MonadReusability m ) - => [Selection (DenormalizedField a) (DenormalizedSelectionSet a)] - -> m (DenormalizedSelectionSet a) + => [NormalizedSelectionSet a] + -> m (NormalizedSelectionSet a) fromObjectSelectionSet :: G.NamedType @@ -61,8 +63,9 @@ class HasSelectionSet a where -> G.NamedType -- ^ fragment typename -> Set.HashSet G.NamedType - -> DenormalizedSelectionSet ObjTyInfo - -> DenormalizedSelectionSet a + -- ^ common types + -> NormalizedSelectionSet ObjTyInfo + -> NormalizedSelectionSet a fromInterfaceSelectionSet :: G.NamedType @@ -70,8 +73,8 @@ class HasSelectionSet a where -> G.NamedType -- ^ fragment typename -> Set.HashSet G.NamedType - -> DenormalizedSelectionSet IFaceTyInfo - -> DenormalizedSelectionSet a + -> NormalizedSelectionSet IFaceTyInfo + -> NormalizedSelectionSet a fromUnionSelectionSet :: G.NamedType @@ -79,10 +82,11 @@ class HasSelectionSet a where -> G.NamedType -- ^ fragment typename -> Set.HashSet G.NamedType - -> DenormalizedSelectionSet UnionTyInfo - -> DenormalizedSelectionSet a + -- ^ common types + -> NormalizedSelectionSet UnionTyInfo + -> NormalizedSelectionSet a -denormalizeObjectSelectionSet +parseObjectSelectionSet :: ( MonadError QErr m , MonadReusability m ) @@ -90,11 +94,19 @@ denormalizeObjectSelectionSet -> ObjTyInfo -> G.SelectionSet -> m ObjectSelectionSet -denormalizeObjectSelectionSet validationCtx objectTypeInfo selectionSet = +parseObjectSelectionSet validationCtx objectTypeInfo selectionSet = flip evalStateT [] $ flip runReaderT validationCtx $ - denormalizeSelectionSet objectTypeInfo selectionSet + parseSelectionSet objectTypeInfo selectionSet + +selectionToSelectionSet + :: HasSelectionSet a + => NormalizedSelection a -> NormalizedSelectionSet a +selectionToSelectionSet = \case + SelectionField alias fld -> fieldToSelectionSet alias fld + SelectionInlineFragmentSpread selectionSet -> selectionSet + SelectionFragmentSpread _ selectionSet -> selectionSet -denormalizeSelectionSet +parseSelectionSet :: ( MonadReader ValidationCtx m , MonadError QErr m , MonadReusability m @@ -103,16 +115,17 @@ denormalizeSelectionSet ) => a -> G.SelectionSet - -> m (DenormalizedSelectionSet a) -denormalizeSelectionSet fieldTypeInfo selectionSet = + -> m (NormalizedSelectionSet a) +parseSelectionSet fieldTypeInfo selectionSet = withPathK "selectionSet" $ do - resolvedSelections <- catMaybes <$> - mapM (denormalizeSelection fieldTypeInfo) selectionSet - mergeSelections resolvedSelections + normalizedSelections <- catMaybes <$> mapM (parseSelection fieldTypeInfo) selectionSet + mergeNormalizedSelections normalizedSelections + where + mergeNormalizedSelections = mergeNormalizedSelectionSets . map selectionToSelectionSet -- | While interfaces and objects have fields, unions do not, so -- this is a specialized function for every Object type -denormalizeSelection +parseSelection :: ( MonadReader ValidationCtx m , MonadError QErr m , MonadReusability m @@ -121,24 +134,24 @@ denormalizeSelection ) => a -- parent type info -> G.Selection - -> m (Maybe (DenormalizedSelection a)) -denormalizeSelection parentTypeInfo = \case + -> m (Maybe (NormalizedSelection a)) +parseSelection parentTypeInfo = \case G.SelectionField fld -> withPathK (G.unName $ G._fName fld) $ do let fieldName = G._fName fld fieldAlias = fromMaybe (G.Alias fieldName) $ G._fAlias fld - fmap (SelectionField fieldAlias) <$> denormalizeField_ parentTypeInfo fld + fmap (SelectionField fieldAlias) <$> parseField_ parentTypeInfo fld G.SelectionFragmentSpread (G.FragmentSpread name directives) -> do FragDef _ fragmentTyInfo fragmentSelectionSet <- getFragmentInfo name withPathK (G.unName name) $ fmap (SelectionFragmentSpread name) <$> - denormalizeFragment parentTypeInfo fragmentTyInfo directives fragmentSelectionSet + parseFragment parentTypeInfo fragmentTyInfo directives fragmentSelectionSet G.SelectionInlineFragment (G.InlineFragment {..}) -> do let fragmentType = fromMaybe (getTypename parentTypeInfo) _ifTypeCondition fragmentTyInfo <- getFragmentTyInfo fragmentType withPathK "inlineFragment" $ fmap SelectionInlineFragmentSpread <$> - denormalizeFragment parentTypeInfo fragmentTyInfo _ifDirectives _ifSelectionSet + parseFragment parentTypeInfo fragmentTyInfo _ifDirectives _ifSelectionSet -denormalizeFragment +parseFragment :: ( MonadReader ValidationCtx m , MonadError QErr m , MonadReusability m @@ -149,22 +162,22 @@ denormalizeFragment -> FragmentTypeInfo -> [G.Directive] -> G.SelectionSet - -> m (Maybe (DenormalizedSelectionSet a)) -denormalizeFragment parentTyInfo fragmentTyInfo directives fragmentSelectionSet = do + -> m (Maybe (NormalizedSelectionSet a)) +parseFragment parentTyInfo fragmentTyInfo directives fragmentSelectionSet = do commonTypes <- validateSpread case fragmentTyInfo of FragmentTyObject objTyInfo -> withDirectives directives $ fmap (fromObjectSelectionSet parentType fragmentType commonTypes) $ - denormalizeSelectionSet objTyInfo fragmentSelectionSet + parseSelectionSet objTyInfo fragmentSelectionSet FragmentTyInterface interfaceTyInfo -> withDirectives directives $ fmap (fromInterfaceSelectionSet parentType fragmentType commonTypes) $ - denormalizeSelectionSet interfaceTyInfo fragmentSelectionSet + parseSelectionSet interfaceTyInfo fragmentSelectionSet FragmentTyUnion unionTyInfo -> withDirectives directives $ fmap (fromUnionSelectionSet parentType fragmentType commonTypes) $ - denormalizeSelectionSet unionTyInfo fragmentSelectionSet + parseSelectionSet unionTyInfo fragmentSelectionSet where validateSpread = do let commonTypes = parentTypeMembers `Set.intersection` fragmentTypeMembers @@ -256,10 +269,22 @@ mergeFields mergeFields flds = AliasedFields <$> OMap.traverseWithKey checkFieldMergeability groups where - -- groups :: OMap.InsOrdHashMap G.Alias (NE.NESeq f) groups = foldr (OMap.unionWith (<>)) mempty $ map (fmap NE.init . unAliasedFields) flds +appendSelectionSets + :: (MonadError QErr m) => SelectionSet -> SelectionSet -> m SelectionSet +appendSelectionSets = curry \case + (SelectionSetObject s1, SelectionSetObject s2) -> + SelectionSetObject <$> mergeObjectSelectionSets [s1, s2] + (SelectionSetInterface s1, SelectionSetInterface s2) -> + SelectionSetInterface <$> appendScopedSelectionSet s1 s2 + (SelectionSetUnion s1, SelectionSetUnion s2) -> + SelectionSetUnion <$> appendScopedSelectionSet s1 s2 + (SelectionSetNone, SelectionSetNone) -> pure SelectionSetNone + (_, _) -> throw500 $ "mergeSelectionSets: 'same kind' assertion failed" + + -- query q { -- author { -- id @@ -276,29 +301,7 @@ mergeSelectionSets :: (MonadError QErr m) => NE.NESeq SelectionSet -> m SelectionSet -- mergeSelectionSets = curry $ \case mergeSelectionSets selectionSets = - case NE.head selectionSets of - SelectionSetObject s -> - SelectionSetObject <$> - assertingSimilar s getObjectSelectionSet mergeObjectSelectionSets - SelectionSetUnion s -> - SelectionSetUnion <$> - assertingSimilar s getUnionSelectionSet mergeUnionSelectionSets - SelectionSetInterface s -> - SelectionSetInterface <$> - assertingSimilar s getInterfaceSelectionSet mergeInterfaceSelectionSets - SelectionSetNone -> - if all (== SelectionSetNone) $ NE.tail selectionSets - then pure SelectionSetNone - else throw500 $ "mergeSelectionSets: 'same kind' assertion failed" - where - assertingSimilar - :: MonadError QErr m - => s -> (SelectionSet -> Maybe s) -> ([s] -> m s) -> m s - assertingSimilar s l f = do - let sameSelectionSets = mapMaybe l $ toList $ NE.tail selectionSets - if length sameSelectionSets == length (NE.tail selectionSets) - then f (s:sameSelectionSets) - else throw500 $ "mergeSelectionSets: 'same kind' assertion failed" + foldM appendSelectionSets (NE.head selectionSets) $ NE.tail selectionSets mergeObjectSelectionSets :: (MonadError QErr m) => [ObjectSelectionSet] -> m ObjectSelectionSet @@ -337,14 +340,6 @@ mergeScopedSelectionSets mergeScopedSelectionSets selectionSets = foldM appendScopedSelectionSet emptyScopedSelectionSet selectionSets -mergeInterfaceSelectionSets - :: (MonadError QErr m) => [InterfaceSelectionSet] -> m InterfaceSelectionSet -mergeInterfaceSelectionSets = mergeScopedSelectionSets - -mergeUnionSelectionSets - :: (MonadError QErr m) => [UnionSelectionSet] -> m UnionSelectionSet -mergeUnionSelectionSets = mergeScopedSelectionSets - withDirectives :: ( MonadReader ValidationCtx m , MonadError QErr m @@ -353,18 +348,19 @@ withDirectives => [G.Directive] -> m a -> m (Maybe a) -withDirectives dirs act = withPathK "directives" $ do - dirDefs <- onLeft (mkMapWith G._dName dirs) $ \dups -> - throwVE $ "the following directives are used more than once: " <> - showNames dups - - procDirs <- flip Map.traverseWithKey dirDefs $ \name dir -> - withPathK (G.unName name) $ do - dirInfo <- onNothing (Map.lookup (G._dName dir) defDirectivesMap) $ - throwVE $ "unexpected directive: " <> showName name - procArgs <- withPathK "args" $ parseArguments (_diParams dirInfo) - (G._dArguments dir) - getIfArg procArgs +withDirectives dirs act = do + procDirs <- withPathK "directives" $ do + dirDefs <- onLeft (mkMapWith G._dName dirs) $ \dups -> + throwVE $ "the following directives are used more than once: " <> + showNames dups + + flip Map.traverseWithKey dirDefs $ \name dir -> + withPathK (G.unName name) $ do + dirInfo <- onNothing (Map.lookup (G._dName dir) defDirectivesMap) $ + throwVE $ "unexpected directive: " <> showName name + procArgs <- withPathK "args" $ parseArguments (_diParams dirInfo) + (G._dArguments dir) + getIfArg procArgs let shouldSkip = fromMaybe False $ Map.lookup "skip" procDirs shouldInclude = fromMaybe True $ Map.lookup "include" procDirs @@ -423,21 +419,21 @@ denormalizeField fldInfo (G.Field _ name args dirs selSet) = do <> G.showGT fldTy <> " must have a selection of subfields" (TIObj objTyInfo, _) -> - SelectionSetObject <$> denormalizeSelectionSet objTyInfo selSet + SelectionSetObject <$> parseSelectionSet objTyInfo selSet (TIIFace _, []) -> throwVE $ "field " <> showName name <> " of type " <> G.showGT fldTy <> " must have a selection of subfields" (TIIFace interfaceTyInfo, _) -> - SelectionSetInterface <$> denormalizeSelectionSet interfaceTyInfo selSet + SelectionSetInterface <$> parseSelectionSet interfaceTyInfo selSet (TIUnion _, []) -> throwVE $ "field " <> showName name <> " of type " <> G.showGT fldTy <> " must have a selection of subfields" (TIUnion unionTyInfo, _) -> - SelectionSetUnion <$> denormalizeSelectionSet unionTyInfo selSet + SelectionSetUnion <$> parseSelectionSet unionTyInfo selSet (TIScalar _, []) -> return SelectionSetNone -- when scalar/enum and no empty set @@ -471,29 +467,26 @@ denormalizeField fldInfo (G.Field _ name args dirs selSet) = do -- when (fldTy /= fragmentType) $ -- throwVE $ "inline fragment is expected on type " <> -- showNamedTy fldTy <> " but found " <> showNamedTy fragmentType --- withDirectives directives $ denormalizeObjectSelectionSet fldTyInfo selSet +-- withDirectives directives $ parseObjectSelectionSet fldTyInfo selSet -- where -- G.InlineFragment tyM directives selSet = inlnFrag -type instance DenormalizedSelectionSet ObjTyInfo = ObjectSelectionSet -type instance DenormalizedField ObjTyInfo = Field +type instance NormalizedSelectionSet ObjTyInfo = ObjectSelectionSet +type instance NormalizedField ObjTyInfo = Field + instance HasSelectionSet ObjTyInfo where getTypename = _otiName getMemberTypes = Set.singleton . _otiName - denormalizeField_ objTyInfo field = do + parseField_ objTyInfo field = do fieldInfo <- getFieldInfo (_otiName objTyInfo) (_otiFields objTyInfo) $ G._fName field denormalizeField fieldInfo field - mergeSelections selections = - mergeObjectSelectionSets $ map toObjectSelectionSet selections - where - toObjectSelectionSet = \case - SelectionField alias fld -> - ObjectSelectionSet $ AliasedFields $ OMap.singleton alias fld - SelectionInlineFragmentSpread selectionSet -> selectionSet - SelectionFragmentSpread _ selectionSet -> selectionSet + fieldToSelectionSet alias fld = + ObjectSelectionSet $ AliasedFields $ OMap.singleton alias fld + + mergeNormalizedSelectionSets = mergeObjectSelectionSets fromObjectSelectionSet _ _ _ objectSelectionSet = objectSelectionSet @@ -504,8 +497,8 @@ instance HasSelectionSet ObjTyInfo where fromUnionSelectionSet parentType _ _ unionSelectionSet = getMemberSelectionSet parentType unionSelectionSet -type instance DenormalizedSelectionSet IFaceTyInfo = InterfaceSelectionSet -type instance DenormalizedField IFaceTyInfo = Field +type instance NormalizedSelectionSet IFaceTyInfo = InterfaceSelectionSet +type instance NormalizedField IFaceTyInfo = Field instance HasSelectionSet IFaceTyInfo where @@ -513,19 +506,15 @@ instance HasSelectionSet IFaceTyInfo where -- TODO getMemberTypes = _ifMemberTypes - denormalizeField_ interfaceTyInfo field = do + parseField_ interfaceTyInfo field = do fieldInfo <- getFieldInfo (_ifName interfaceTyInfo) (_ifFields interfaceTyInfo) $ G._fName field denormalizeField fieldInfo field - mergeSelections selections = - mergeInterfaceSelectionSets $ map toInterfaceSelectionSet selections - where - toInterfaceSelectionSet = \case - SelectionField alias fld -> - ScopedSelectionSet (AliasedFields $ OMap.singleton alias fld) mempty - SelectionInlineFragmentSpread selectionSet -> selectionSet - SelectionFragmentSpread _ selectionSet -> selectionSet + fieldToSelectionSet alias field = + ScopedSelectionSet (AliasedFields $ OMap.singleton alias field) mempty + + mergeNormalizedSelectionSets = mergeScopedSelectionSets fromObjectSelectionSet _ fragmentType _ objectSelectionSet = ScopedSelectionSet (AliasedFields mempty) $ @@ -541,27 +530,23 @@ instance HasSelectionSet IFaceTyInfo where Map.fromList $ flip map (toList commonTypes) $ \commonType -> (commonType, getMemberSelectionSet commonType unionSelectionSet) -type instance DenormalizedSelectionSet UnionTyInfo = UnionSelectionSet -type instance DenormalizedField UnionTyInfo = Typename +type instance NormalizedSelectionSet UnionTyInfo = UnionSelectionSet +type instance NormalizedField UnionTyInfo = Typename instance HasSelectionSet UnionTyInfo where getTypename = _utiName getMemberTypes = _utiMemberTypes - denormalizeField_ unionTyInfo field = do + parseField_ unionTyInfo field = do let fieldMap = Map.singleton (_fiName typenameFld) typenameFld fieldInfo <- getFieldInfo (_utiName unionTyInfo) fieldMap $ G._fName field fmap (const Typename) <$> denormalizeField fieldInfo field - mergeSelections selections = - mergeUnionSelectionSets $ map toUnionSelectionSet selections - where - toUnionSelectionSet = \case - SelectionField alias fld -> - ScopedSelectionSet (AliasedFields $ OMap.singleton alias fld) mempty - SelectionInlineFragmentSpread selectionSet -> selectionSet - SelectionFragmentSpread _ selectionSet -> selectionSet + fieldToSelectionSet alias field = + ScopedSelectionSet (AliasedFields $ OMap.singleton alias field) mempty + + mergeNormalizedSelectionSets = mergeScopedSelectionSets fromObjectSelectionSet _ fragmentType _ objectSelectionSet = ScopedSelectionSet (AliasedFields mempty) $ From fb87e2835f99409c9cb78201e0d3c2ccf99c5fa4 Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi Date: Mon, 11 May 2020 13:12:46 +0530 Subject: [PATCH 07/29] minor cleanup --- .../Hasura/GraphQL/Validate/SelectionSet.hs | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs index c7831b3a2411f..1a78840c26231 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs @@ -182,7 +182,8 @@ parseFragment parentTyInfo fragmentTyInfo directives fragmentSelectionSet = do validateSpread = do let commonTypes = parentTypeMembers `Set.intersection` fragmentTypeMembers if null commonTypes then - -- TODO: fragment source + -- TODO: better error location by capturing the fragment source - + -- named or otherwise -- throwVE $ "cannot spread fragment " <> showName name <> " defined on " <> throwVE $ "cannot spread fragment defined on " <> showNamedTy fragmentType <> " when selecting fields of type " <> showNamedTy parentType @@ -452,25 +453,6 @@ denormalizeField fldInfo (G.Field _ name args dirs selSet) = do withDirectives dirs $ pure $ Field name fldBaseTy argMap fields --- inlineFragmentInObjectScope --- :: ( MonadReader ValidationCtx m --- , MonadError QErr m --- , MonadReusability m --- , MonadState [G.Name] m --- ) --- => ObjTyInfo -- type information of the field --- -> G.InlineFragment --- -> m (Maybe ObjectSelectionSet) --- inlineFragmentInObjectScope fldTyInfo inlnFrag = do --- let fldTy = _otiName fldTyInfo --- let fragmentType = fromMaybe fldTy tyM --- when (fldTy /= fragmentType) $ --- throwVE $ "inline fragment is expected on type " <> --- showNamedTy fldTy <> " but found " <> showNamedTy fragmentType --- withDirectives directives $ parseObjectSelectionSet fldTyInfo selSet --- where --- G.InlineFragment tyM directives selSet = inlnFrag - type instance NormalizedSelectionSet ObjTyInfo = ObjectSelectionSet type instance NormalizedField ObjTyInfo = Field @@ -503,7 +485,6 @@ type instance NormalizedField IFaceTyInfo = Field instance HasSelectionSet IFaceTyInfo where getTypename = _ifName - -- TODO getMemberTypes = _ifMemberTypes parseField_ interfaceTyInfo field = do From 8e28ed3d8d14df84480268421d6705454972dd45 Mon Sep 17 00:00:00 2001 From: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Date: Fri, 15 May 2020 17:19:32 +0530 Subject: [PATCH 08/29] generate connection SQL with complete Relay features ** ../DML/Select/* modules refactor -> Brand new functions and types -> Refactor the Annotated internal AST with better names --- server/src-lib/Hasura/GraphQL/Resolve.hs | 20 +- .../src-lib/Hasura/GraphQL/Resolve/Action.hs | 26 +- .../Hasura/GraphQL/Resolve/Mutation.hs | 2 +- .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 277 ++- .../src-lib/Hasura/GraphQL/Resolve/Types.hs | 12 +- server/src-lib/Hasura/GraphQL/Schema.hs | 68 +- .../src-lib/Hasura/GraphQL/Schema/Action.hs | 2 +- .../src-lib/Hasura/GraphQL/Schema/Common.hs | 17 +- .../src-lib/Hasura/GraphQL/Schema/OrderBy.hs | 36 +- .../src-lib/Hasura/GraphQL/Schema/Select.hs | 51 +- server/src-lib/Hasura/RQL/DML/Mutation.hs | 4 +- server/src-lib/Hasura/RQL/DML/Returning.hs | 32 +- server/src-lib/Hasura/RQL/DML/Select.hs | 24 +- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 1696 ++++++++++------- server/src-lib/Hasura/RQL/DML/Select/Types.hs | 512 ++--- server/src-lib/Hasura/RQL/Types/Common.hs | 1 + server/src-lib/Hasura/SQL/DML.hs | 25 +- server/src-lib/Hasura/SQL/Rewrite.hs | 20 +- server/src-lib/Hasura/Server/Utils.hs | 15 + 19 files changed, 1654 insertions(+), 1186 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve.hs b/server/src-lib/Hasura/GraphQL/Resolve.hs index 7f3404f63a569..235b838d29b33 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve.hs @@ -43,8 +43,8 @@ import qualified Hasura.SQL.DML as S data QueryRootFldAST v = QRFPk !(DS.AnnSimpleSelG v) | QRFSimple !(DS.AnnSimpleSelG v) - | QRFAgg !(DS.AnnAggSelG v) - | QRFConnection !(Maybe (NonEmpty PGCol)) !(DS.ConnectionSelect v) + | QRFAgg !(DS.AnnAggregateSelectG v) + | QRFConnection !(DS.ConnectionSelect v) | QRFActionSelect !(DS.AnnSimpleSelG v) deriving (Show, Eq) @@ -57,11 +57,11 @@ traverseQueryRootFldAST -> QueryRootFldAST a -> f (QueryRootFldAST b) traverseQueryRootFldAST f = \case - QRFPk s -> QRFPk <$> DS.traverseAnnSimpleSel f s - QRFSimple s -> QRFSimple <$> DS.traverseAnnSimpleSel f s - QRFAgg s -> QRFAgg <$> DS.traverseAnnAggSel f s - QRFConnection pkey s -> QRFConnection pkey <$> DS.traverseConnectionSelect f s - QRFActionSelect s -> QRFActionSelect <$> DS.traverseAnnSimpleSel f s + QRFPk s -> QRFPk <$> DS.traverseAnnSimpleSelect f s + QRFSimple s -> QRFSimple <$> DS.traverseAnnSimpleSelect f s + QRFAgg s -> QRFAgg <$> DS.traverseAnnAggregateSelect f s + QRFConnection s -> QRFConnection <$> DS.traverseConnectionSelect f s + QRFActionSelect s -> QRFActionSelect <$> DS.traverseAnnSimpleSelect f s toPGQuery :: QueryRootFldResolved -> Q.Query toPGQuery = \case @@ -69,7 +69,7 @@ toPGQuery = \case QRFSimple s -> DS.selectQuerySQL DS.JASMultipleRows s QRFAgg s -> DS.selectAggQuerySQL s QRFActionSelect s -> DS.selectQuerySQL DS.JASSingleObject s - QRFConnection pkey s -> Q.fromBuilder $ toSQL $ DS.mkConnectionSelect pkey s + QRFConnection s -> Q.fromBuilder $ toSQL $ DS.mkConnectionSelect s validateHdrs :: (Foldable t, QErrM m) => UserInfo -> t Text -> m () @@ -108,9 +108,9 @@ queryFldToPGAST fld = do QCActionFetch ctx -> QRFActionSelect <$> RA.resolveAsyncActionQuery userInfo ctx fld QCSelectConnection pk ctx -> - QRFConnection pk <$> RS.convertConnectionSelect ctx fld + QRFConnection <$> RS.convertConnectionSelect pk ctx fld QCFuncConnection pk ctx -> - QRFConnection pk <$> RS.convertConnectionFuncQuery ctx fld + QRFConnection <$> RS.convertConnectionFuncQuery pk ctx fld mutFldToTx :: ( HasVersion diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Action.hs b/server/src-lib/Hasura/GraphQL/Resolve/Action.hs index 6f782a179d9a0..482a810742731 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Action.hs @@ -135,7 +135,7 @@ resolveActionMutationSync field executionContext sessionVariables = do selectAstUnresolved <- processOutputSelectionSet webhookResponseExpression outputType definitionList (_fType field) $ _fSelSet field - astResolved <- RS.traverseAnnSimpleSel resolveValTxt selectAstUnresolved + astResolved <- RS.traverseAnnSimpleSelect resolveValTxt selectAstUnresolved let jsonAggType = mkJsonAggSelect outputType return $ (,respHeaders) $ asSingleRowJsonResp (RS.selectQuerySQL jsonAggType astResolved) [] where @@ -211,35 +211,35 @@ resolveAsyncActionQuery userInfo selectOpCtx field = do annotatedFields <- fmap (map (first FieldName)) $ withSelSet (_fSelSet field) $ \fld -> case _fName fld of - "__typename" -> return $ RS.FExp $ G.unName $ G.unNamedType $ _fType field + "__typename" -> return $ RS.AFExpression $ G.unName $ G.unNamedType $ _fType field "output" -> do -- See Note [Resolving async action query/subscription] let inputTableArgument = RS.AETableRow $ Just $ Iden "response_payload" ActionSelectOpContext outputType definitionList = selectOpCtx jsonAggSelect = mkJsonAggSelect outputType - (RS.FComputedField . RS.CFSTable jsonAggSelect) + (RS.AFComputedField . RS.CFSTable jsonAggSelect) <$> processOutputSelectionSet inputTableArgument outputType definitionList (_fType fld) (_fSelSet fld) -- The metadata columns - "id" -> return $ mkAnnFldFromPGCol "id" PGUUID - "created_at" -> return $ mkAnnFldFromPGCol "created_at" PGTimeStampTZ - "errors" -> return $ mkAnnFldFromPGCol "errors" PGJSONB + "id" -> return $ mkAnnFieldFromPGCol "id" PGUUID + "created_at" -> return $ mkAnnFieldFromPGCol "created_at" PGTimeStampTZ + "errors" -> return $ mkAnnFieldFromPGCol "errors" PGJSONB G.Name t -> throw500 $ "unexpected field in actions' httpResponse : " <> t let tableFromExp = RS.FromTable actionLogTable - tableArguments = RS.noTableArgs - { RS._taWhere = Just $ mkTableBoolExpression actionId} + tableArguments = RS.noSelectArgs + { RS._saWhere = Just $ mkTableBoolExpression actionId} tablePermissions = RS.TablePerm annBoolExpTrue Nothing - selectAstUnresolved = RS.AnnSelG annotatedFields tableFromExp tablePermissions + selectAstUnresolved = RS.AnnSelectG annotatedFields tableFromExp tablePermissions tableArguments stringifyNumerics return selectAstUnresolved where actionLogTable = QualifiedObject (SchemaName "hdb_catalog") (TableName "hdb_action_log") -- TODO:- Avoid using PGColumnInfo - mkAnnFldFromPGCol column columnType = - flip RS.mkAnnColField Nothing $ + mkAnnFieldFromPGCol column columnType = + flip RS.mkAnnColumnField Nothing $ PGColumnInfo (unsafePGCol column) (G.Name column) 0 (PGColumnScalar columnType) True Nothing parseActionId annInpValue = mkParameterizablePGValue <$> asPGColumnValue annInpValue @@ -469,8 +469,8 @@ processOutputSelectionSet processOutputSelectionSet tableRowInput actionOutputType definitionList fldTy flds = do stringifyNumerics <- stringifyNum <$> asks getter annotatedFields <- processTableSelectionSet fldTy flds - let annSel = RS.AnnSelG annotatedFields selectFrom - RS.noTablePermissions RS.noTableArgs stringifyNumerics + let annSel = RS.AnnSelectG annotatedFields selectFrom + RS.noTablePermissions RS.noSelectArgs stringifyNumerics pure annSel where jsonbToPostgresRecordFunction = diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs index bfcb9d992eb85..d3ccbff7e9b8d 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs @@ -50,7 +50,7 @@ resolveMutationFields ty selSet = fmap (map (first FieldName)) $ "returning" -> do annFlds <- processTableSelectionSet (_fType fld) $ _fSelSet fld annFldsResolved <- traverse - (traverse (RS.traverseAnnFld convertPGValueToTextValue)) annFlds + (traverse (RS.traverseAnnField convertPGValueToTextValue)) annFlds return $ RR.MRet annFldsResolved G.Name t -> throw500 $ "unexpected field in mutation resp : " <> t where diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index c31072f9e3d9c..d47c48543e059 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -16,11 +16,15 @@ import Data.Has import Data.Parser.JSONPath import Hasura.Prelude +import qualified Data.Aeson as J +import qualified Data.Aeson.Internal as J +import qualified Data.ByteString.Lazy as BL import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.List.NonEmpty as NE import qualified Data.Sequence as Seq import qualified Data.Text as T +import qualified Data.Text.Conversions as TC import qualified Language.GraphQL.Draft.Syntax as G import qualified Hasura.RQL.DML.Select as RS @@ -29,11 +33,12 @@ import qualified Hasura.SQL.DML as S import Hasura.GraphQL.Resolve.BoolExp import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue -import Hasura.GraphQL.Schema (isAggFld) +import Hasura.GraphQL.Schema (isAggregateField) import Hasura.GraphQL.Validate.Field import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Internal (onlyPositiveInt) import Hasura.RQL.Types +import Hasura.Server.Utils import Hasura.SQL.Types import Hasura.SQL.Value @@ -46,27 +51,27 @@ jsonPathToColExp t = case parseJSONPath t of elToColExp (Index i) = S.SELit $ T.pack (show i) -argsToColOp :: (MonadReusability m, MonadError QErr m) => ArgsMap -> m (Maybe RS.ColOp) -argsToColOp args = maybe (return Nothing) toOp $ Map.lookup "path" args +argsToColumnOp :: (MonadReusability m, MonadError QErr m) => ArgsMap -> m (Maybe RS.ColumnOp) +argsToColumnOp args = maybe (return Nothing) toOp $ Map.lookup "path" args where - toJsonPathExp = fmap (RS.ColOp S.jsonbPathOp) . jsonPathToColExp + toJsonPathExp = fmap (RS.ColumnOp S.jsonbPathOp) . jsonPathToColExp toOp v = asPGColTextM v >>= traverse toJsonPathExp -type AnnFlds = RS.AnnFldsG UnresolvedVal +type AnnFields = RS.AnnFieldsG UnresolvedVal resolveComputedField :: ( MonadReusability m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r, MonadError QErr m ) - => ComputedField -> Field -> m (RS.ComputedFieldSel UnresolvedVal) + => ComputedField -> Field -> m (RS.ComputedFieldSelect UnresolvedVal) resolveComputedField computedField fld = fieldAsPath fld $ do funcArgs <- parseFunctionArgs argSeq argFn $ Map.lookup "args" $ _fArguments fld let argsWithTableArgument = withTableArgument funcArgs case fieldType of CFTScalar scalarTy -> do - colOpM <- argsToColOp $ _fArguments fld + colOpM <- argsToColumnOp $ _fArguments fld pure $ RS.CFSScalar $ - RS.ComputedFieldScalarSel qf argsWithTableArgument scalarTy colOpM + RS.ComputedFieldScalarSelect qf argsWithTableArgument scalarTy colOpM CFTTable (ComputedFieldTable _ cols permFilter permLimit) -> do let functionFrom = RS.FromFunction qf argsWithTableArgument Nothing RS.CFSTable RS.JASMultipleRows <$> fromField functionFrom cols permFilter permLimit fld @@ -87,48 +92,52 @@ processTableSelectionSet :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => G.NamedType -> SelSet -> m AnnFlds + => G.NamedType -> SelSet -> m AnnFields processTableSelectionSet fldTy flds = forM (toList flds) $ \fld -> do let fldName = _fName fld let rqlFldName = FieldName $ G.unName $ G.unAlias $ _fAlias fld (rqlFldName,) <$> case fldName of - "__typename" -> return $ RS.FExp $ G.unName $ G.unNamedType fldTy + "__typename" -> return $ RS.AFExpression $ G.unName $ G.unNamedType fldTy _ -> do fldInfo <- getFldInfo fldTy fldName case fldInfo of RFPGColumn colInfo -> - RS.mkAnnColField colInfo <$> argsToColOp (_fArguments fld) + RS.mkAnnColumnField colInfo <$> argsToColumnOp (_fArguments fld) RFComputedField computedField -> - RS.FComputedField <$> resolveComputedField computedField fld - RFRelationship (RelationshipField relInfo isAgg colGNameMap tableFilter tableLimit) -> do + RS.AFComputedField <$> resolveComputedField computedField fld + RFRelationship (RelationshipField relInfo fieldKind colGNameMap tableFilter tableLimit) -> do let relTN = riRTable relInfo colMapping = riMapping relInfo rn = riName relInfo - if isAgg then do - aggSel <- fromAggField (RS.FromTable relTN) colGNameMap tableFilter tableLimit fld - return $ RS.FArr $ RS.ASAgg $ RS.AnnRelG rn colMapping aggSel - else do - annSel <- fromField (RS.FromTable relTN) colGNameMap tableFilter tableLimit fld - let annRel = RS.AnnRelG rn colMapping annSel - return $ case riType relInfo of - ObjRel -> RS.FObj annRel - ArrRel -> RS.FArr $ RS.ASSimple annRel - -type TableAggFlds = RS.TableAggFldsG UnresolvedVal + case fieldKind of + RFKSimple -> do + annSel <- fromField (RS.FromTable relTN) colGNameMap tableFilter tableLimit fld + let annRel = RS.AnnRelationSelectG rn colMapping annSel + pure $ case riType relInfo of + ObjRel -> RS.AFObjectRelation annRel + ArrRel -> RS.AFArrayRelation $ RS.ASSimple annRel + RFKAggregate -> do + aggSel <- fromAggField (RS.FromTable relTN) colGNameMap tableFilter tableLimit fld + pure $ RS.AFArrayRelation $ RS.ASAggregate $ RS.AnnRelationSelectG rn colMapping aggSel + RFKConnection pkCols -> do + connSel <- fromConnectionField (RS.FromTable relTN) pkCols tableFilter tableLimit fld + pure $ RS.AFArrayRelation $ RS.ASConnection $ RS.AnnRelationSelectG rn colMapping connSel + +type TableAggregateFields = RS.TableAggregateFieldsG UnresolvedVal fromAggSelSet :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => PGColGNameMap -> G.NamedType -> SelSet -> m TableAggFlds + => PGColGNameMap -> G.NamedType -> SelSet -> m TableAggregateFields fromAggSelSet colGNameMap fldTy selSet = fmap toFields $ withSelSet selSet $ \f -> do let fTy = _fType f fSelSet = _fSelSet f case _fName f of "__typename" -> return $ RS.TAFExp $ G.unName $ G.unNamedType fldTy - "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet + "aggregate" -> RS.TAFAgg <$> convertAggregateField colGNameMap fTy fSelSet "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet G.Name t -> throw500 $ "unexpected field in _agg node: " <> t @@ -145,7 +154,7 @@ fromConnectionSelSet fldTy selSet = fmap toFields $ "__typename" -> return $ RS.ConnectionTypename $ G.unName $ G.unNamedType fldTy "pageInfo" -> RS.ConnectionPageInfo <$> parsePageInfoSelectionSet fTy fSelSet "edges" -> RS.ConnectionEdges <$> parseEdgeSelectionSet fTy fSelSet - -- "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet + -- "aggregate" -> RS.TAFAgg <$> convertAggregateField colGNameMap fTy fSelSet -- "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet G.Name t -> throw500 $ "unexpected field in _connection node: " <> t @@ -160,7 +169,7 @@ parseEdgeSelectionSet fldTy selSet = fmap toFields $ fSelSet = _fSelSet f case _fName f of "__typename" -> pure $ RS.EdgeTypename $ G.unName $ G.unNamedType fldTy - "cursor" -> pure $ RS.EdgeCursor + "cursor" -> pure RS.EdgeCursor "node" -> RS.EdgeNode <$> processTableSelectionSet fTy fSelSet G.Name t -> throw500 $ "unexpected field in Edge node: " <> t @@ -171,40 +180,41 @@ parsePageInfoSelectionSet fldTy selSet = fmap toFields $ withSelSet selSet $ \f -> case _fName f of "__typename" -> pure $ RS.PageInfoTypename $ G.unName $ G.unNamedType fldTy - "hasNextPage" -> pure $ RS.PageInfoHasNextPage - "hasPreviousPage" -> pure $ RS.PageInfoHasPreviousPage - "startCursor" -> pure $ RS.PageInfoStartCursor - "endCursor" -> pure $ RS.PageInfoEndCursor - -- "aggregate" -> RS.TAFAgg <$> convertAggFld colGNameMap fTy fSelSet + "hasNextPage" -> pure RS.PageInfoHasNextPage + "hasPreviousPage" -> pure RS.PageInfoHasPreviousPage + "startCursor" -> pure RS.PageInfoStartCursor + "endCursor" -> pure RS.PageInfoEndCursor + -- "aggregate" -> RS.TAFAgg <$> convertAggregateField colGNameMap fTy fSelSet -- "nodes" -> RS.TAFNodes <$> processTableSelectionSet fTy fSelSet G.Name t -> throw500 $ "unexpected field in PageInfo node: " <> t -type TableArgs = RS.TableArgsG UnresolvedVal +type SelectArgs = RS.SelectArgsG UnresolvedVal -parseTableArgs +parseSelectArgs :: ( MonadReusability m, MonadError QErr m, MonadReader r m , Has FieldMap r, Has OrdByCtx r ) - => PGColGNameMap -> ArgsMap -> m TableArgs -parseTableArgs colGNameMap args = do + => PGColGNameMap -> ArgsMap -> m SelectArgs +parseSelectArgs colGNameMap args = do whereExpM <- withArgM args "where" parseBoolExp ordByExpML <- withArgM args "order_by" parseOrderBy let ordByExpM = NE.nonEmpty =<< ordByExpML - limitExpM <- withArgM args "limit" parseLimit + limitExpM <- withArgM args "limit" $ + parseNonNegativeInt "expecting Integer value for \"limit\"" offsetExpM <- withArgM args "offset" $ asPGColumnValue >=> openOpaqueValue >=> txtConverter distOnColsML <- withArgM args "distinct_on" $ parseColumns colGNameMap let distOnColsM = NE.nonEmpty =<< distOnColsML mapM_ (validateDistOn ordByExpM) distOnColsM - return $ RS.TableArgs whereExpM ordByExpM limitExpM offsetExpM distOnColsM + return $ RS.SelectArgs whereExpM ordByExpM limitExpM offsetExpM distOnColsM where validateDistOn Nothing _ = return () validateDistOn (Just ordBys) cols = withPathK "args" $ do let colsLen = length cols initOrdBys = take colsLen $ toList ordBys initOrdByCols = flip mapMaybe initOrdBys $ \ob -> - case obiColumn ob of - RS.AOCPG pgCol -> Just pgCol - _ -> Nothing + case obiColumn ob of + RS.AOCColumn pgCol -> Just $ pgiColumn pgCol + _ -> Nothing isValid = (colsLen == length initOrdByCols) && all (`elem` initOrdByCols) (toList cols) @@ -223,12 +233,12 @@ fromField -> Maybe Int -> Field -> m AnnSimpleSelect fromField selFrom colGNameMap permFilter permLimitM fld = fieldAsPath fld $ do - tableArgs <- parseTableArgs colGNameMap args + tableArgs <- parseSelectArgs colGNameMap args annFlds <- processTableSelectionSet (_fType fld) $ _fSelSet fld let unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter let tabPerm = RS.TablePerm unresolvedPermFltr permLimitM strfyNum <- stringifyNum <$> asks getter - return $ RS.AnnSelG annFlds selFrom tabPerm tableArgs strfyNum + return $ RS.AnnSelectG annFlds selFrom tabPerm tableArgs strfyNum where args = _fArguments fld @@ -249,7 +259,8 @@ parseOrderBy , MonadReader r m , Has OrdByCtx r ) - => AnnInpVal -> m [RS.AnnOrderByItemG UnresolvedVal] + => AnnInpVal + -> m [RS.AnnOrderByItemG UnresolvedVal] parseOrderBy = fmap concat . withArray f where f _ = mapM (withObject (getAnnObItems id)) @@ -260,7 +271,7 @@ getAnnObItems , MonadReader r m , Has OrdByCtx r ) - => (RS.AnnObColG UnresolvedVal -> RS.AnnObColG UnresolvedVal) + => (RS.AnnOrderByElement UnresolvedVal -> RS.AnnOrderByElement UnresolvedVal) -> G.NamedType -> AnnGObject -> m [RS.AnnOrderByItemG UnresolvedVal] @@ -272,7 +283,7 @@ getAnnObItems f nt obj = do <> showNamedTy nt <> " map" case ordByItem of OBIPGCol ci -> do - let aobCol = f $ RS.AOCPG $ pgiColumn ci + let aobCol = f $ RS.AOCColumn ci (_, enumValM) <- asEnumValM v ordByItemM <- forM enumValM $ \enumVal -> do (ordTy, nullsOrd) <- parseOrderByEnum enumVal @@ -281,13 +292,13 @@ getAnnObItems f nt obj = do OBIRel ri fltr -> do let unresolvedFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal fltr - let annObColFn = f . RS.AOCObj ri unresolvedFltr + let annObColFn = f . RS.AOCObjectRelation ri unresolvedFltr flip withObjectM v $ \nameTy objM -> maybe (pure []) (getAnnObItems annObColFn nameTy) objM OBIAgg ri relColGNameMap fltr -> do let unresolvedFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal fltr - let aobColFn = f . RS.AOCAgg ri unresolvedFltr + let aobColFn = f . RS.AOCArrayAggregation ri unresolvedFltr flip withObjectM v $ \_ objM -> maybe (pure []) (parseAggOrdBy relColGNameMap aobColFn) objM @@ -298,7 +309,7 @@ mkOrdByItemG ordTy aobCol nullsOrd = parseAggOrdBy :: (MonadReusability m, MonadError QErr m) => PGColGNameMap - -> (RS.AnnAggOrdBy -> RS.AnnObColG UnresolvedVal) + -> (RS.AnnAggregateOrderBy -> RS.AnnOrderByElement UnresolvedVal) -> AnnGObject -> m [RS.AnnOrderByItemG UnresolvedVal] parseAggOrdBy colGNameMap f annObj = @@ -311,14 +322,14 @@ parseAggOrdBy colGNameMap f annObj = return $ mkOrdByItemG ordTy (f RS.AAOCount) nullsOrd return $ maybe [] pure ordByItemM - G.Name opT -> + G.Name opText -> flip withObject obVal $ \_ opObObj -> fmap catMaybes $ forM (OMap.toList opObObj) $ \(colName, eVal) -> do (_, enumValM) <- asEnumValM eVal forM enumValM $ \enumVal -> do (ordTy, nullsOrd) <- parseOrderByEnum enumVal - col <- pgiColumn <$> resolvePGCol colGNameMap colName - let aobCol = f $ RS.AAOOp opT col + col <- resolvePGCol colGNameMap colName + let aobCol = f $ RS.AAOOp opText col return $ mkOrdByItemG ordTy aobCol nullsOrd parseOrderByEnum @@ -335,15 +346,14 @@ parseOrderByEnum = \case G.EnumValue v -> throw500 $ "enum value " <> showName v <> " not found in type order_by" -parseLimit :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m Int -parseLimit v = do +parseNonNegativeInt + :: (MonadReusability m, MonadError QErr m) => Text -> AnnInpVal -> m Int +parseNonNegativeInt errMsg v = do pgColVal <- openOpaqueValue =<< asPGColumnValue v - limit <- maybe noIntErr return . pgColValueToInt . pstValue $ _apvValue pgColVal + limit <- maybe (throwVE errMsg) return . pgColValueToInt . pstValue $ _apvValue pgColVal -- validate int value onlyPositiveInt limit return limit - where - noIntErr = throwVE "expecting Integer value for \"limit\"" type AnnSimpleSel = RS.AnnSimpleSelG UnresolvedVal @@ -364,9 +374,9 @@ fromFieldByPKey tn colArgMap permFilter fld = fieldAsPath fld $ do unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter tabPerm = RS.TablePerm unresolvedPermFltr Nothing - tabArgs = RS.noTableArgs { RS._taWhere = Just boolExp} + tabArgs = RS.noSelectArgs { RS._saWhere = Just boolExp} strfyNum <- stringifyNum <$> asks getter - return $ RS.AnnSelG annFlds tabFrom tabPerm tabArgs strfyNum + return $ RS.AnnSelectG annFlds tabFrom tabPerm tabArgs strfyNum where fldTy = _fType fld @@ -423,19 +433,19 @@ convertCount colGNameMap args = do toFields :: [(T.Text, a)] -> RS.Fields a toFields = map (first FieldName) -convertColFlds +convertColumnFields :: (MonadError QErr m) - => PGColGNameMap -> G.NamedType -> SelSet -> m RS.ColFlds -convertColFlds colGNameMap ty selSet = fmap toFields $ + => PGColGNameMap -> G.NamedType -> SelSet -> m RS.ColumnFields +convertColumnFields colGNameMap ty selSet = fmap toFields $ withSelSet selSet $ \fld -> case _fName fld of "__typename" -> return $ RS.PCFExp $ G.unName $ G.unNamedType ty - n -> (RS.PCFCol . pgiColumn) <$> resolvePGCol colGNameMap n + n -> RS.PCFCol . pgiColumn <$> resolvePGCol colGNameMap n -convertAggFld +convertAggregateField :: (MonadReusability m, MonadError QErr m) - => PGColGNameMap -> G.NamedType -> SelSet -> m RS.AggFlds -convertAggFld colGNameMap ty selSet = fmap toFields $ + => PGColGNameMap -> G.NamedType -> SelSet -> m RS.AggregateFields +convertAggregateField colGNameMap ty selSet = fmap toFields $ withSelSet selSet $ \fld -> do let fType = _fType fld fSelSet = _fSelSet fld @@ -443,14 +453,14 @@ convertAggFld colGNameMap ty selSet = fmap toFields $ "__typename" -> return $ RS.AFExp $ G.unName $ G.unNamedType ty "count" -> RS.AFCount <$> convertCount colGNameMap (_fArguments fld) n -> do - colFlds <- convertColFlds colGNameMap fType fSelSet - unless (isAggFld n) $ throwInvalidFld n - return $ RS.AFOp $ RS.AggOp (G.unName n) colFlds + colFlds <- convertColumnFields colGNameMap fType fSelSet + unless (isAggregateField n) $ throwInvalidFld n + return $ RS.AFOp $ RS.AggregateOp (G.unName n) colFlds where throwInvalidFld (G.Name t) = throw500 $ "unexpected field in _aggregate node: " <> t -type AnnAggSel = RS.AnnAggSelG UnresolvedVal +type AnnAggregateSelect = RS.AnnAggregateSelectG UnresolvedVal fromAggField :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r @@ -460,15 +470,15 @@ fromAggField -> PGColGNameMap -> AnnBoolExpPartialSQL -> Maybe Int - -> Field -> m AnnAggSel + -> Field -> m AnnAggregateSelect fromAggField selectFrom colGNameMap permFilter permLimit fld = fieldAsPath fld $ do - tableArgs <- parseTableArgs colGNameMap args + tableArgs <- parseSelectArgs colGNameMap args aggSelFlds <- fromAggSelSet colGNameMap (_fType fld) (_fSelSet fld) let unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter let tabPerm = RS.TablePerm unresolvedPermFltr permLimit strfyNum <- stringifyNum <$> asks getter - return $ RS.AnnSelG aggSelFlds selectFrom tabPerm tableArgs strfyNum + return $ RS.AnnSelectG aggSelFlds selectFrom tabPerm tableArgs strfyNum where args = _fArguments fld @@ -477,37 +487,118 @@ fromConnectionField , Has OrdByCtx r, Has SQLGenCtx r ) => RS.SelectFromG UnresolvedVal + -> NonEmpty PGColumnInfo -> AnnBoolExpPartialSQL -> Maybe Int -> Field -> m (RS.ConnectionSelect UnresolvedVal) -fromConnectionField selectFrom permFilter permLimit fld = fieldAsPath fld $ do - tableArgs <- parseConnectionArgs args - aggSelFlds <- fromConnectionSelSet (_fType fld) (_fSelSet fld) +fromConnectionField selectFrom pkCols permFilter permLimit fld = fieldAsPath fld $ do + (tableArgs, slice, split) <- parseConnectionArgs pkCols args + connSelFlds <- fromConnectionSelSet (_fType fld) (_fSelSet fld) + strfyNum <- stringifyNum <$> asks getter let unresolvedPermFltr = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter - let tabPerm = RS.TablePerm unresolvedPermFltr permLimit - strfyNum <- stringifyNum <$> asks getter - return $ RS.AnnSelG aggSelFlds selectFrom tabPerm tableArgs strfyNum + tabPerm = RS.TablePerm unresolvedPermFltr permLimit + annSel = RS.AnnSelectG connSelFlds selectFrom tabPerm tableArgs strfyNum + pure $ RS.ConnectionSelect pkCols split slice annSel where args = _fArguments fld parseConnectionArgs - :: ( MonadReusability m, MonadError QErr m, MonadReader r m + :: forall r m. + ( MonadReusability m, MonadError QErr m, MonadReader r m , Has FieldMap r, Has OrdByCtx r ) - => ArgsMap -> m TableArgs -parseConnectionArgs args = do + => NonEmpty PGColumnInfo + -> ArgsMap + -> m ( SelectArgs + , Maybe RS.ConnectionSlice + , Maybe (NE.NonEmpty (RS.ConnectionSplit UnresolvedVal)) + ) +parseConnectionArgs pKeyColumns args = do whereExpM <- withArgM args "where" parseBoolExp ordByExpML <- withArgM args "order_by" parseOrderBy + + slice <- case (Map.lookup "first" args, Map.lookup "last" args) of + (Nothing, Nothing) -> pure Nothing + (Just _, Just _) -> throwVE "\"first\" and \"last\" are not allowed at once" + (Just v, Nothing) -> Just . RS.SliceFirst <$> parseNonNegativeInt + "expecting Integer value for \"first\"" v + (Nothing, Just v) -> Just . RS.SliceLast <$> parseNonNegativeInt + "expecting Integer value for \"last\"" v + + maybeSplit <- case (Map.lookup "after" args, Map.lookup "before" args) of + (Nothing, Nothing) -> pure Nothing + (Just _, Just _) -> throwVE "\"after\" and \"before\" are not allowed at once" + (Just v, Nothing) -> fmap ((RS.CSKAfter,) . TC.convertText) <$> asPGColTextM v + (Nothing, Just v) -> fmap ((RS.CSKBefore,) . TC.convertText) <$> asPGColTextM v + let ordByExpM = NE.nonEmpty =<< ordByExpML - -- limitExpM <- withArgM args "limit" parseLimit - return $ RS.TableArgs whereExpM ordByExpM Nothing Nothing Nothing + tableArgs = RS.SelectArgs whereExpM ordByExpM Nothing Nothing Nothing + + split <- mapM (uncurry (validateConnectionSplit ordByExpM)) maybeSplit + pure (tableArgs, slice, split) + where + validateConnectionSplit + :: Maybe (NonEmpty (RS.AnnOrderByItemG UnresolvedVal)) + -> RS.ConnectionSplitKind + -> Maybe (TC.Base64 BL.ByteString) + -> m (NonEmpty (RS.ConnectionSplit UnresolvedVal)) + validateConnectionSplit maybeOrderBys splitKind maybeCursorSplit = do + cursorSplit <- maybe throwInvalidCursor pure maybeCursorSplit + cursorValue <- either (const throwInvalidCursor) pure $ + J.eitherDecode $ TC.unBase64 cursorSplit + case maybeOrderBys of + Nothing -> forM pKeyColumns $ + \pgColumnInfo -> do + let columnJsonPath = [J.Key $ getPGColTxt $ pgiColumn pgColumnInfo] + pgColumnValue <- maybe throwInvalidCursor pure $ iResultToMaybe $ + executeJSONPath columnJsonPath cursorValue + pgValue <- parsePGScalarValue (pgiType pgColumnInfo) pgColumnValue + let unresolvedValue = UVPG $ AnnPGVal Nothing False pgValue + pure $ RS.ConnectionSplit splitKind unresolvedValue $ + OrderByItemG Nothing (RS.AOCColumn pgColumnInfo) Nothing + Just orderBys -> + forM orderBys $ \orderBy -> do + let OrderByItemG orderType annObCol nullsOrder = orderBy + orderByItemValue <- maybe throwInvalidCursor pure $ iResultToMaybe $ + executeJSONPath (getPathFromOrderBy annObCol) cursorValue + pgValue <- parsePGScalarValue (getOrderByColumnType annObCol) orderByItemValue + let unresolvedValue = UVPG $ AnnPGVal Nothing False pgValue + pure $ RS.ConnectionSplit splitKind unresolvedValue $ + OrderByItemG orderType (() <$ annObCol) nullsOrder + where + throwInvalidCursor = throwVE "the \"after\" or \"before\" cursor is invalid" + + iResultToMaybe = \case + J.ISuccess v -> Just v + J.IError{} -> Nothing + + getPathFromOrderBy = \case + RS.AOCColumn pgColInfo -> + let pathElement = J.Key $ getPGColTxt $ pgiColumn pgColInfo + in [pathElement] + RS.AOCObjectRelation relInfo _ obCol -> + let pathElement = J.Key $ relNameToTxt $ riName relInfo + in pathElement : getPathFromOrderBy obCol + RS.AOCArrayAggregation relInfo _ aggOb -> + let fieldName = J.Key $ relNameToTxt (riName relInfo) <> "_aggregate" + in fieldName : case aggOb of + RS.AAOCount -> [J.Key "count"] + RS.AAOOp t col -> [J.Key t, J.Key $ getPGColTxt $ pgiColumn col] + + getOrderByColumnType = \case + RS.AOCColumn pgColInfo -> pgiType pgColInfo + RS.AOCObjectRelation _ _ obCol -> getOrderByColumnType obCol + RS.AOCArrayAggregation _ _ aggOb -> + case aggOb of + RS.AAOCount -> PGColumnScalar PGInteger + RS.AAOOp _ colInfo -> pgiType colInfo convertAggSelect :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => SelOpCtx -> Field -> m (RS.AnnAggSelG UnresolvedVal) + => SelOpCtx -> Field -> m (RS.AnnAggregateSelectG UnresolvedVal) convertAggSelect opCtx fld = withPathK "selectionSet" $ fromAggField (RS.FromTable qt) colGNameMap permFilter permLimit fld @@ -518,10 +609,10 @@ convertConnectionSelect :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r , Has OrdByCtx r, Has SQLGenCtx r ) - => SelOpCtx -> Field -> m (RS.ConnectionSelect UnresolvedVal) -convertConnectionSelect opCtx fld = + => NonEmpty PGColumnInfo -> SelOpCtx -> Field -> m (RS.ConnectionSelect UnresolvedVal) +convertConnectionSelect pkCols opCtx fld = withPathK "selectionSet" $ - fromConnectionField (RS.FromTable qt) permFilter permLimit fld + fromConnectionField (RS.FromTable qt) pkCols permFilter permLimit fld where SelOpCtx qt _ _ permFilter permLimit = opCtx @@ -600,7 +691,7 @@ convertFuncQueryAgg , Has OrdByCtx r , Has SQLGenCtx r ) - => FuncQOpCtx -> Field -> m AnnAggSel + => FuncQOpCtx -> Field -> m AnnAggregateSelect convertFuncQueryAgg funcOpCtx fld = withPathK "selectionSet" $ fieldAsPath fld $ do selectFrom <- makeFunctionSelectFrom qf argSeq fld @@ -616,10 +707,10 @@ convertConnectionFuncQuery , Has OrdByCtx r , Has SQLGenCtx r ) - => FuncQOpCtx -> Field -> m (RS.ConnectionSelect UnresolvedVal) -convertConnectionFuncQuery funcOpCtx fld = + => NonEmpty PGColumnInfo -> FuncQOpCtx -> Field -> m (RS.ConnectionSelect UnresolvedVal) +convertConnectionFuncQuery pkCols funcOpCtx fld = withPathK "selectionSet" $ fieldAsPath fld $ do selectFrom <- makeFunctionSelectFrom qf argSeq fld - fromConnectionField selectFrom permFilter permLimit fld + fromConnectionField selectFrom pkCols permFilter permLimit fld where FuncQOpCtx qf argSeq _ _ permFilter permLimit = funcOpCtx diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs index 297778a9a0e62..79461ea059589 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs @@ -29,12 +29,12 @@ import qualified Hasura.SQL.DML as S data QueryCtx = QCSelect !SelOpCtx - | QCSelectConnection !(Maybe (NonEmpty PGCol)) !SelOpCtx + | QCSelectConnection !(NonEmpty PGColumnInfo) !SelOpCtx | QCSelectPkey !SelPkOpCtx | QCSelectAgg !SelOpCtx | QCFuncQuery !FuncQOpCtx | QCFuncAggQuery !FuncQOpCtx - | QCFuncConnection !(Maybe (NonEmpty PGCol)) !FuncQOpCtx + | QCFuncConnection !(NonEmpty PGColumnInfo) !FuncQOpCtx | QCActionFetch !ActionSelectOpContext deriving (Show, Eq) @@ -131,10 +131,16 @@ data ActionSelectOpContext -- used in resolvers type PGColGNameMap = Map.HashMap G.Name PGColumnInfo +data RelationshipFieldKind + = RFKAggregate + | RFKSimple + | RFKConnection !(NonEmpty PGColumnInfo) + deriving (Show, Eq) + data RelationshipField = RelationshipField { _rfInfo :: !RelInfo - , _rfIsAgg :: !Bool + , _rfIsAgg :: !RelationshipFieldKind , _rfCols :: !PGColGNameMap , _rfPermFilter :: !AnnBoolExpPartialSQL , _rfPermLimit :: !(Maybe Int) diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index af1ac0082fd29..46420c6573d53 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -8,7 +8,7 @@ module Hasura.GraphQL.Schema , InsCtx(..) , InsCtxMap , RelationInfoMap - , isAggFld + , isAggregateField , qualObjectToName , ppGCtx @@ -100,16 +100,16 @@ mkPGColGNameMap :: [PGColumnInfo] -> PGColGNameMap mkPGColGNameMap cols = Map.fromList $ flip map cols $ \ci -> (pgiName ci, ci) -numAggOps :: [G.Name] -numAggOps = [ "sum", "avg", "stddev", "stddev_samp", "stddev_pop" +numAggregateOps :: [G.Name] +numAggregateOps = [ "sum", "avg", "stddev", "stddev_samp", "stddev_pop" , "variance", "var_samp", "var_pop" ] -compAggOps :: [G.Name] -compAggOps = ["max", "min"] +compAggregateOps :: [G.Name] +compAggregateOps = ["max", "min"] -isAggFld :: G.Name -> Bool -isAggFld = flip elem (numAggOps <> compAggOps) +isAggregateField :: G.Name -> Bool +isAggregateField = flip elem (numAggregateOps <> compAggregateOps) mkComputedFieldFunctionArgSeq :: Seq.Seq FunctionArg -> ComputedFieldFunctionArgSeq mkComputedFieldFunctionArgSeq inputArgs = @@ -225,17 +225,22 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi mkFldMap ty = Map.fromList . concatMap (mkFld ty) mkFld ty = \case SFPGColumn ci -> [((ty, pgiName ci), RFPGColumn ci)] - SFRelationship (RelationshipFieldInfo relInfo allowAgg cols permFilter permLimit _) -> + SFRelationship (RelationshipFieldInfo relInfo allowAgg cols permFilter permLimit maybePkCols _) -> let relationshipName = riName relInfo relFld = ( (ty, mkRelName relationshipName) - , RFRelationship $ RelationshipField relInfo False cols permFilter permLimit + , RFRelationship $ RelationshipField relInfo RFKSimple cols permFilter permLimit ) aggRelFld = ( (ty, mkAggRelName relationshipName) - , RFRelationship $ RelationshipField relInfo True cols permFilter permLimit + , RFRelationship $ RelationshipField relInfo RFKAggregate cols permFilter permLimit ) + maybeConnFld = maybePkCols <&> \pkCols -> + ( (ty, mkConnectionRelName relationshipName) + , RFRelationship $ RelationshipField relInfo + (RFKConnection pkCols) cols permFilter permLimit + ) in case riType relInfo of ObjRel -> [relFld] - ArrRel -> bool [relFld] [relFld, aggRelFld] allowAgg + ArrRel -> bool [relFld] ([relFld, aggRelFld] <> maybe [] pure maybeConnFld) allowAgg SFComputedField cf -> pure ( (ty, mkComputedFieldName $ _cfName cf) , RFComputedField cf @@ -269,10 +274,10 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi numCols = onlyNumCols cols compCols = onlyComparableCols cols objs = [ mkTableAggObj tn - , mkTableAggFldsObj tn (numCols, numAggOps) (compCols, compAggOps) - ] <> mkColAggFldsObjs selFlds - ordByInps = mkTabAggOrdByInpObj tn (numCols, numAggOps) (compCols, compAggOps) - : mkTabAggOpOrdByInpObjs tn (numCols, numAggOps) (compCols, compAggOps) + , mkTableAggregateFieldsObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) + ] <> mkColAggregateFieldsObjs selFlds + ordByInps = mkTabAggOrdByInpObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) + : mkTabAggregateOpOrdByInpObjs tn (numCols, numAggregateOps) (compCols, compAggregateOps) in (objs, ordByInps) _ -> ([], []) @@ -283,13 +288,13 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi mkTypeMaker "sum" = mkColumnType mkTypeMaker _ = onlyFloat - mkColAggFldsObjs flds = + mkColAggregateFieldsObjs flds = let numCols = getNumericCols flds compCols = getComparableCols flds - mkNumObjFld n = mkTableColAggFldsObj tn n (mkTypeMaker n) numCols - mkCompObjFld n = mkTableColAggFldsObj tn n mkColumnType compCols - numFldsObjs = bool (map mkNumObjFld numAggOps) [] $ null numCols - compFldsObjs = bool (map mkCompObjFld compAggOps) [] $ null compCols + mkNumObjFld n = mkTableColAggregateFieldsObj tn n (mkTypeMaker n) numCols + mkCompObjFld n = mkTableColAggregateFieldsObj tn n mkColumnType compCols + numFldsObjs = bool (map mkNumObjFld numAggregateOps) [] $ null numCols + compFldsObjs = bool (map mkCompObjFld compAggregateOps) [] $ null compCols in numFldsObjs <> compFldsObjs -- the fields used in table object selObjFldsM = mkFldMap (mkTableTy tn) <$> selFldsM @@ -349,7 +354,7 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM <> funcAggQueries <> catMaybes [ getSelDet <$> selM - , getSelConnectionDet <$> selM + , getSelConnectionDet <$> selM <*> maybePrimaryKeyColumns , getSelAggDet selM , getPKeySelDet <$> selM <*> primaryKey ] @@ -363,13 +368,14 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM ] } where - primaryKeyColumn = fmap (fmap pgiColumn . _pkColumns) primaryKey + maybePrimaryKeyColumns = fmap _pkColumns primaryKey makeFieldMap = mapFromL (_fiName . snd) customRootFields = _tcCustomRootFields tableConfig colGNameMap = mkPGColGNameMap $ getCols fields funcQueries = maybe [] getFuncQueryFlds selM - funcConnectionQueries = maybe [] getFuncQueryConnectionFlds selM + funcConnectionQueries = fromMaybe [] $ getFuncQueryConnectionFlds + <$> selM <*> maybePrimaryKeyColumns funcAggQueries = maybe [] getFuncAggQueryFlds selM mutHelper :: (ViewInfo -> Bool) -> (a -> b) -> Maybe a -> Maybe b @@ -421,8 +427,9 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM selCustName = getCustomNameWith _tcrfSelect getSelDet (selFltr, pLimit, hdrs, _) = selFldHelper QCSelect (mkSelFld selCustName) selFltr pLimit hdrs - getSelConnectionDet (selFltr, pLimit, hdrs, _) = - selFldHelper (QCSelectConnection primaryKeyColumn) (mkSelFldConnection Nothing) selFltr pLimit hdrs + getSelConnectionDet (selFltr, pLimit, hdrs, _) primaryKeyColumns = + selFldHelper (QCSelectConnection primaryKeyColumns) + (mkSelFldConnection Nothing) selFltr pLimit hdrs selAggCustName = getCustomNameWith _tcrfSelectAggregate getSelAggDet (Just (selFltr, pLimit, hdrs, True)) = @@ -445,8 +452,8 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM getFuncQueryFlds (selFltr, pLimit, hdrs, _) = funcFldHelper QCFuncQuery mkFuncQueryFld selFltr pLimit hdrs - getFuncQueryConnectionFlds (selFltr, pLimit, hdrs, _) = - funcFldHelper (QCFuncConnection primaryKeyColumn) mkFuncQueryConnectionFld selFltr pLimit hdrs + getFuncQueryConnectionFlds (selFltr, pLimit, hdrs, _) primaryKeyColumns = + funcFldHelper (QCFuncConnection primaryKeyColumns) mkFuncQueryConnectionFld selFltr pLimit hdrs getFuncAggQueryFlds (selFltr, pLimit, hdrs, True) = funcFldHelper QCFuncAggQuery mkFuncAggQueryFld selFltr pLimit hdrs @@ -496,6 +503,8 @@ getSelPerm tableCache fields role selPermInfo = do , _rfiColumns = remTableColGNameMap , _rfiPermFilter = spiFilter rmSelPermM , _rfiPermLimit = spiLimit rmSelPermM + , _rfiPrimaryKeyColumns = _pkColumns <$> + _tciPrimaryKey (_tiCoreInfo remTableInfo) , _rfiIsNullable = isRelNullable fields relInfo } @@ -608,6 +617,7 @@ mkAdminSelFlds fields tableCache = do , _rfiColumns = remoteTableColGNameMap , _rfiPermFilter = noFilter , _rfiPermLimit = Nothing + , _rfiPrimaryKeyColumns = _pkColumns <$> _tciPrimaryKey remoteTableInfo , _rfiIsNullable = isRelNullable fields relInfo } @@ -658,7 +668,7 @@ mkGCtxRole tableCache tn descM fields primaryKey constraints funcs viM tabConfig return (ctx, (permCols, icRelations ctx)) let insPermM = snd <$> tabInsInfoM insCtxM = fst <$> tabInsInfoM - updColsM = filterColFlds . upiCols <$> _permUpd permInfo + updColsM = filterColumnFields . upiCols <$> _permUpd permInfo tyAgg = mkGCtxRole' tn descM insPermM selPermM updColsM (void $ _permDel permInfo) primaryKey constraints viM funcs rootFlds = getRootFldsRole tn primaryKey constraints fields funcs @@ -668,7 +678,7 @@ mkGCtxRole tableCache tn descM fields primaryKey constraints funcs viM tabConfig where allCols = getCols fields cols = getValidCols fields - filterColFlds allowedSet = + filterColumnFields allowedSet = filter ((`Set.member` allowedSet) . pgiColumn) cols getRootFldsRole diff --git a/server/src-lib/Hasura/GraphQL/Schema/Action.hs b/server/src-lib/Hasura/GraphQL/Schema/Action.hs index f39df90b95d9c..aa874172e1701 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Action.hs @@ -204,7 +204,7 @@ mkActionFieldsAndTypes actionInfo annotatedOutputType permission = (RelName $ mkNonEmptyTextUnsafe $ coerce relationshipName) (_trType relationship) columnMapping remoteTable True) - False mempty + RFKSimple mempty tableFilter tableLimit ) diff --git a/server/src-lib/Hasura/GraphQL/Schema/Common.hs b/server/src-lib/Hasura/GraphQL/Schema/Common.hs index bf3ede49f1ce6..c46f327258256 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Common.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Common.hs @@ -13,6 +13,7 @@ module Hasura.GraphQL.Schema.Common , mkColumnType , mkRelName , mkAggRelName + , mkConnectionRelName , mkComputedFieldName , mkTableTy @@ -43,12 +44,13 @@ import Hasura.SQL.Types data RelationshipFieldInfo = RelationshipFieldInfo - { _rfiInfo :: !RelInfo - , _rfiAllowAgg :: !Bool - , _rfiColumns :: !PGColGNameMap - , _rfiPermFilter :: !AnnBoolExpPartialSQL - , _rfiPermLimit :: !(Maybe Int) - , _rfiIsNullable :: !Bool + { _rfiInfo :: !RelInfo + , _rfiAllowAgg :: !Bool + , _rfiColumns :: !PGColGNameMap + , _rfiPermFilter :: !AnnBoolExpPartialSQL + , _rfiPermLimit :: !(Maybe Int) + , _rfiPrimaryKeyColumns :: !(Maybe (NonEmpty PGColumnInfo)) + , _rfiIsNullable :: !Bool } deriving (Show, Eq) data SelField @@ -82,6 +84,9 @@ mkRelName rn = G.Name $ relNameToTxt rn mkAggRelName :: RelName -> G.Name mkAggRelName rn = G.Name $ relNameToTxt rn <> "_aggregate" +mkConnectionRelName :: RelName -> G.Name +mkConnectionRelName rn = G.Name $ relNameToTxt rn <> "_connection" + mkComputedFieldName :: ComputedFieldName -> G.Name mkComputedFieldName = G.Name . computedFieldNameToText diff --git a/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs b/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs index cb648dda6c69f..f8e5f9cf35e56 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs @@ -3,7 +3,7 @@ module Hasura.GraphQL.Schema.OrderBy , ordByEnumTy , mkOrdByInpObj , mkTabAggOrdByInpObj - , mkTabAggOpOrdByInpObjs + , mkTabAggregateOpOrdByInpObjs ) where import Control.Arrow ((&&&)) @@ -50,8 +50,8 @@ ordByEnumTy = ) ] -mkTabAggOpOrdByTy :: QualifiedTable -> G.Name -> G.NamedType -mkTabAggOpOrdByTy tn op = +mkTabAggregateOpOrdByTy :: QualifiedTable -> G.Name -> G.NamedType +mkTabAggregateOpOrdByTy tn op = G.NamedType $ qualObjectToName tn <> "_" <> op <> "_order_by" {- @@ -62,14 +62,14 @@ input table__order_by { } -} -mkTabAggOpOrdByInpObjs +mkTabAggregateOpOrdByInpObjs :: QualifiedTable -> ([PGColumnInfo], [G.Name]) -> ([PGColumnInfo], [G.Name]) -> [InpObjTyInfo] -mkTabAggOpOrdByInpObjs tn (numCols, numAggOps) (compCols, compAggOps) = - mapMaybe (mkInpObjTyM numCols) numAggOps - <> mapMaybe (mkInpObjTyM compCols) compAggOps +mkTabAggregateOpOrdByInpObjs tn (numCols, numAggregateOps) (compCols, compAggregateOps) = + mapMaybe (mkInpObjTyM numCols) numAggregateOps + <> mapMaybe (mkInpObjTyM compCols) compAggregateOps where mkDesc (G.Name op) = @@ -77,7 +77,7 @@ mkTabAggOpOrdByInpObjs tn (numCols, numAggOps) (compCols, compAggOps) = mkInpObjTyM cols op = bool (Just $ mkInpObjTy cols op) Nothing $ null cols mkInpObjTy cols op = - mkHsraInpTyInfo (Just $ mkDesc op) (mkTabAggOpOrdByTy tn op) $ + mkHsraInpTyInfo (Just $ mkDesc op) (mkTabAggregateOpOrdByTy tn op) $ fromInpValL $ map mkColInpVal cols mkColInpVal ci = InpValInfo Nothing (pgiName ci) Nothing $ G.toGT @@ -99,17 +99,17 @@ mkTabAggOrdByInpObj -> ([PGColumnInfo], [G.Name]) -> ([PGColumnInfo], [G.Name]) -> InpObjTyInfo -mkTabAggOrdByInpObj tn (numCols, numAggOps) (compCols, compAggOps) = +mkTabAggOrdByInpObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) = mkHsraInpTyInfo (Just desc) (mkTabAggOrdByTy tn) $ fromInpValL $ numOpOrdBys <> compOpOrdBys <> [countInpVal] where desc = G.Description $ "order by aggregate values of table " <>> tn - numOpOrdBys = bool (map mkInpValInfo numAggOps) [] $ null numCols - compOpOrdBys = bool (map mkInpValInfo compAggOps) [] $ null compCols + numOpOrdBys = bool (map mkInpValInfo numAggregateOps) [] $ null numCols + compOpOrdBys = bool (map mkInpValInfo compAggregateOps) [] $ null compCols mkInpValInfo op = InpValInfo Nothing op Nothing $ G.toGT $ - mkTabAggOpOrdByTy tn op + mkTabAggregateOpOrdByTy tn op countInpVal = InpValInfo Nothing "count" Nothing $ G.toGT ordByTy @@ -134,14 +134,14 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) where inpObjTy = mkHsraInpTyInfo (Just desc) namedTy $ fromInpValL $ - map mkColOrdBy pgColFlds <> map mkObjRelOrdBy objRels - <> mapMaybe mkArrRelAggOrdBy arrRels + map mkColOrdBy pgColumnFields <> map mkObjRelOrdBy objRels + <> mapMaybe mkArrayAggregateSelectOrdBy arrRels namedTy = mkOrdByTy tn desc = G.Description $ "ordering options when selecting data from " <>> tn - pgColFlds = getPGColumnFields selFlds + pgColumnFields = getPGColumnFields selFlds relFltr ty = flip filter (getRelationshipFields selFlds) $ \rf -> riType (_rfiInfo rf) == ty objRels = relFltr ObjRel @@ -154,7 +154,7 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) in InpValInfo Nothing (mkRelName $ riName ri) Nothing $ G.toGT $ mkOrdByTy $ riRTable ri - mkArrRelAggOrdBy relationshipField = + mkArrayAggregateSelectOrdBy relationshipField = let ri = _rfiInfo relationshipField isAggAllowed = _rfiAllowAgg relationshipField ivi = InpValInfo Nothing (mkAggRelName $ riName ri) Nothing $ @@ -163,7 +163,7 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) ordByCtx = Map.singleton namedTy $ Map.fromList $ colOrdBys <> relOrdBys <> arrRelOrdBys - colOrdBys = map (pgiName &&& OBIPGCol) pgColFlds + colOrdBys = map (pgiName &&& OBIPGCol) pgColumnFields relOrdBys = flip map objRels $ \relationshipField -> let ri = _rfiInfo relationshipField @@ -173,7 +173,7 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) ) arrRelOrdBys = flip mapMaybe arrRels $ - \(RelationshipFieldInfo ri isAggAllowed colGNameMap fltr _ _) -> + \(RelationshipFieldInfo ri isAggAllowed colGNameMap fltr _ _ _) -> let obItem = ( mkAggRelName $ riName ri , OBIAgg ri colGNameMap fltr ) diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index 5ae4744e86d21..c41d8f7220559 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -2,8 +2,8 @@ module Hasura.GraphQL.Schema.Select ( mkTableObj , mkTableAggObj , mkSelColumnTy - , mkTableAggFldsObj - , mkTableColAggFldsObj + , mkTableAggregateFieldsObj + , mkTableColAggregateFieldsObj , mkTableEdgeObj , mkPageInfoObj , mkTableConnectionObj @@ -45,11 +45,11 @@ mkSelColumnInpTy :: QualifiedTable -> G.NamedType mkSelColumnInpTy tn = G.NamedType $ qualObjectToName tn <> "_select_column" -mkTableAggFldsTy :: QualifiedTable -> G.NamedType -mkTableAggFldsTy = addTypeSuffix "_aggregate_fields" . mkTableTy +mkTableAggregateFieldsTy :: QualifiedTable -> G.NamedType +mkTableAggregateFieldsTy = addTypeSuffix "_aggregate_fields" . mkTableTy -mkTableColAggFldsTy :: G.Name -> QualifiedTable -> G.NamedType -mkTableColAggFldsTy op tn = +mkTableColAggregateFieldsTy :: G.Name -> QualifiedTable -> G.NamedType +mkTableColAggregateFieldsTy op tn = G.NamedType $ qualObjectToName tn <> "_" <> op <> "_fields" mkTableByPkName :: QualifiedTable -> G.Name @@ -166,10 +166,11 @@ object_relationship: remote_table mkRelationshipField :: Bool -> RelInfo + -> Maybe (NonEmpty PGColumnInfo) -> Bool -> [ObjFldInfo] -mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) isNullable = case rTy of - ArrRel -> bool [arrRelFld] [arrRelFld, arrConnectionFld, aggArrRelFld] allowAgg +mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) maybePkCols isNullable = case rTy of + ArrRel -> bool [arrRelFld] ([arrRelFld, aggArrRelFld] <> arrConnectionFld) allowAgg ObjRel -> [objRelFld] where objRelFld = mkHsraObjFldInfo (Just "An object relationship") @@ -183,8 +184,8 @@ mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) isNullable = cas (fromInpValL $ mkSelArgs remTab) $ G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy remTab - arrConnectionFld = - mkHsraObjFldInfo Nothing (mkRelName rn <> "_connection") + arrConnectionFld = if isNothing maybePkCols then [] else pure $ + mkHsraObjFldInfo Nothing (mkConnectionRelName rn) (fromInpValL $ mkConnectionArgs remTab) $ G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableConnectionTy remTab @@ -208,12 +209,12 @@ mkTableObj mkTableObj tn descM allowedFlds = mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) TLHasuraType where - flds = pgColFlds <> relFlds <> computedFlds - pgColFlds = map mkPGColFld $ getPGColumnFields allowedFlds + flds = pgColumnFields <> relFlds <> computedFlds + pgColumnFields = map mkPGColFld $ getPGColumnFields allowedFlds relFlds = concatMap mkRelationshipField' $ getRelationshipFields allowedFlds computedFlds = map mkComputedFieldFld $ getComputedFields allowedFlds - mkRelationshipField' (RelationshipFieldInfo relInfo allowAgg _ _ _ isNullable) = - mkRelationshipField allowAgg relInfo isNullable + mkRelationshipField' (RelationshipFieldInfo relInfo allowAgg _ _ _ maybePkCols isNullable) = + mkRelationshipField allowAgg relInfo maybePkCols isNullable desc = mkDescriptionWith descM $ "columns and relationships of " <>> tn {- @@ -232,7 +233,7 @@ mkTableAggObj tn = "aggregated selection of " <>> tn aggFld = mkHsraObjFldInfo Nothing "aggregate" Map.empty $ G.toGT $ - mkTableAggFldsTy tn + mkTableAggregateFieldsTy tn nodesFld = mkHsraObjFldInfo Nothing "nodes" Map.empty $ G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy tn @@ -249,13 +250,13 @@ type table_aggregate_fields{ min: table_min_fields } -} -mkTableAggFldsObj +mkTableAggregateFieldsObj :: QualifiedTable -> ([PGColumnInfo], [G.Name]) -> ([PGColumnInfo], [G.Name]) -> ObjTyInfo -mkTableAggFldsObj tn (numCols, numAggOps) (compCols, compAggOps) = - mkHsraObjTyInfo (Just desc) (mkTableAggFldsTy tn) Set.empty $ mapFromL _fiName $ +mkTableAggregateFieldsObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) = + mkHsraObjTyInfo (Just desc) (mkTableAggregateFieldsTy tn) Set.empty $ mapFromL _fiName $ countFld : (numFlds <> compFlds) where desc = G.Description $ @@ -271,11 +272,11 @@ mkTableAggFldsObj tn (numCols, numAggOps) (compCols, compAggOps) = distinctInpVal = InpValInfo Nothing "distinct" Nothing $ G.toGT $ mkScalarTy PGBoolean - numFlds = bool (map mkColOpFld numAggOps) [] $ null numCols - compFlds = bool (map mkColOpFld compAggOps) [] $ null compCols + numFlds = bool (map mkColumnOpFld numAggregateOps) [] $ null numCols + compFlds = bool (map mkColumnOpFld compAggregateOps) [] $ null compCols - mkColOpFld op = mkHsraObjFldInfo Nothing op Map.empty $ G.toGT $ - mkTableColAggFldsTy op tn + mkColumnOpFld op = mkHsraObjFldInfo Nothing op Map.empty $ G.toGT $ + mkTableColAggregateFieldsTy op tn {- type table__fields{ @@ -284,14 +285,14 @@ type table__fields{ . . } -} -mkTableColAggFldsObj +mkTableColAggregateFieldsObj :: QualifiedTable -> G.Name -> (PGColumnType -> G.NamedType) -> [PGColumnInfo] -> ObjTyInfo -mkTableColAggFldsObj tn op f cols = - mkHsraObjTyInfo (Just desc) (mkTableColAggFldsTy op tn) Set.empty $ mapFromL _fiName $ +mkTableColAggregateFieldsObj tn op f cols = + mkHsraObjTyInfo (Just desc) (mkTableColAggregateFieldsTy op tn) Set.empty $ mapFromL _fiName $ map mkColObjFld cols where desc = G.Description $ "aggregate " <> G.unName op <> " on columns" diff --git a/server/src-lib/Hasura/RQL/DML/Mutation.hs b/server/src-lib/Hasura/RQL/DML/Mutation.hs index 9438880a09f2b..bd75e3582bcfc 100644 --- a/server/src-lib/Hasura/RQL/DML/Mutation.hs +++ b/server/src-lib/Hasura/RQL/DML/Mutation.hs @@ -70,7 +70,7 @@ mutateAndFetchCols qt cols (cte, p) strfyNum = tabFrom = FromIden aliasIden tabPerm = TablePerm annBoolExpTrue Nothing selFlds = flip map cols $ - \ci -> (fromPGCol $ pgiColumn ci, mkAnnColFieldAsText ci) + \ci -> (fromPGCol $ pgiColumn ci, mkAnnColumnFieldAsText ci) sql = toSQL selectWith selectWith = S.SelectWith [(S.Alias aliasIden, cte)] select @@ -86,7 +86,7 @@ mutateAndFetchCols qt cols (cte, p) strfyNum = , S.selFrom = Just $ S.FromExp [S.FIIden aliasIden] } colSel = S.SESelect $ mkSQLSelect JASMultipleRows $ - AnnSelG selFlds tabFrom tabPerm noTableArgs strfyNum + AnnSelectG selFlds tabFrom tabPerm noSelectArgs strfyNum -- | Note:- Using sorted columns is necessary to enable casting the rows returned by VALUES expression to table type. -- For example, let's consider the table, `CREATE TABLE test (id serial primary key, name text not null, age int)`. diff --git a/server/src-lib/Hasura/RQL/DML/Returning.hs b/server/src-lib/Hasura/RQL/DML/Returning.hs index 344fc79426a43..cb4f112ff2794 100644 --- a/server/src-lib/Hasura/RQL/DML/Returning.hs +++ b/server/src-lib/Hasura/RQL/DML/Returning.hs @@ -12,7 +12,7 @@ import qualified Hasura.SQL.DML as S data MutFldG v = MCount | MExp !T.Text - | MRet !(AnnFldsG v) + | MRet !(AnnFieldsG v) deriving (Show, Eq) traverseMutFld @@ -23,7 +23,7 @@ traverseMutFld traverseMutFld f = \case MCount -> pure MCount MExp t -> pure $ MExp t - MRet flds -> MRet <$> traverse (traverse (traverseAnnFld f)) flds + MRet flds -> MRet <$> traverse (traverse (traverseAnnField f)) flds type MutFld = MutFldG S.SQLExp @@ -31,7 +31,7 @@ type MutFldsG v = Fields (MutFldG v) data MutationOutputG v = MOutMultirowFields !(MutFldsG v) - | MOutSinglerowObject !(AnnFldsG v) + | MOutSinglerowObject !(AnnFieldsG v) deriving (Show, Eq) traverseMutationOutput @@ -42,7 +42,7 @@ traverseMutationOutput f = \case MOutMultirowFields mutationFields -> MOutMultirowFields <$> traverse (traverse (traverseMutFld f)) mutationFields MOutSinglerowObject annFields -> - MOutSinglerowObject <$> traverseAnnFlds f annFields + MOutSinglerowObject <$> traverseAnnFields f annFields type MutationOutput = MutationOutputG S.SQLExp @@ -59,15 +59,15 @@ type MutFlds = MutFldsG S.SQLExp hasNestedFld :: MutationOutputG a -> Bool hasNestedFld = \case MOutMultirowFields flds -> any isNestedMutFld flds - MOutSinglerowObject annFlds -> any isNestedAnnFld annFlds + MOutSinglerowObject annFlds -> any isNestedAnnField annFlds where isNestedMutFld (_, mutFld) = case mutFld of - MRet annFlds -> any isNestedAnnFld annFlds + MRet annFlds -> any isNestedAnnField annFlds _ -> False - isNestedAnnFld (_, annFld) = case annFld of - FObj _ -> True - FArr _ -> True - _ -> False + isNestedAnnField (_, annFld) = case annFld of + AFObjectRelation _ -> True + AFArrayRelation _ -> True + _ -> False pgColsFromMutFld :: MutFld -> [(PGCol, PGColumnType)] pgColsFromMutFld = \case @@ -75,16 +75,16 @@ pgColsFromMutFld = \case MExp _ -> [] MRet selFlds -> flip mapMaybe selFlds $ \(_, annFld) -> case annFld of - FCol (AnnColField (PGColumnInfo col _ _ colTy _ _) _ _) -> Just (col, colTy) - _ -> Nothing + AFColumn (AnnColumnField (PGColumnInfo col _ _ colTy _ _) _ _) -> Just (col, colTy) + _ -> Nothing pgColsFromMutFlds :: MutFlds -> [(PGCol, PGColumnType)] pgColsFromMutFlds = concatMap (pgColsFromMutFld . snd) -pgColsToSelFlds :: [PGColumnInfo] -> [(FieldName, AnnFld)] +pgColsToSelFlds :: [PGColumnInfo] -> [(FieldName, AnnField)] pgColsToSelFlds cols = flip map cols $ - \pgColInfo -> (fromPGCol $ pgiColumn pgColInfo, mkAnnColField pgColInfo Nothing) + \pgColInfo -> (fromPGCol $ pgiColumn pgColInfo, mkAnnColumnField pgColInfo Nothing) mkDefaultMutFlds :: Maybe [PGColumnInfo] -> MutationOutput mkDefaultMutFlds = MOutMultirowFields . \case @@ -107,7 +107,7 @@ mkMutFldExp cteAlias preCalAffRows strfyNum = \case let tabFrom = FromIden cteAlias tabPerm = TablePerm annBoolExpTrue Nothing in S.SESelect $ mkSQLSelect JASMultipleRows $ - AnnSelG selFlds tabFrom tabPerm noTableArgs strfyNum + AnnSelectG selFlds tabFrom tabPerm noSelectArgs strfyNum {- Note [Mutation output expression] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -171,7 +171,7 @@ mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum = let tabFrom = FromIden allColumnsAlias tabPerm = TablePerm annBoolExpTrue Nothing in S.SESelect $ mkSQLSelect JASSingleObject $ - AnnSelG annFlds tabFrom tabPerm noTableArgs strfyNum + AnnSelectG annFlds tabFrom tabPerm noSelectArgs strfyNum checkRetCols diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index ab8a892b435b2..abfc34f8f34e0 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -105,7 +105,7 @@ convOrderByElem => SessVarBldr m -> (FieldInfoMap FieldInfo, SelPermInfo) -> OrderByCol - -> m AnnObCol + -> m (AnnOrderByElement S.SQLExp) convOrderByElem sessVarBldr (flds, spi) = \case OCPG fldName -> do fldInfo <- askFieldInfo flds fldName @@ -118,7 +118,7 @@ convOrderByElem sessVarBldr (flds, spi) = \case [ fldName <<> " has type 'geometry'" , " and cannot be used in order_by" ] - else return $ AOCPG $ pgiColumn colInfo + else return $ AOCColumn colInfo FIRelationship _ -> throw400 UnexpectedPayload $ mconcat [ fldName <<> " is a" , " relationship and should be expanded" @@ -146,7 +146,7 @@ convOrderByElem sessVarBldr (flds, spi) = \case ] (relFim, relSpi) <- fetchRelDet (riName relInfo) (riRTable relInfo) resolvedSelFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter relSpi - AOCObj relInfo resolvedSelFltr <$> + AOCObjectRelation relInfo resolvedSelFltr <$> convOrderByElem sessVarBldr (relFim, relSpi) rest convSelectQ @@ -163,12 +163,12 @@ convSelectQ fieldInfoMap selPermInfo selQ sessVarBldr prepValBldr = do indexedForM (sqColumns selQ) $ \case (ECSimple pgCol) -> do colInfo <- convExtSimple fieldInfoMap selPermInfo pgCol - return (fromPGCol pgCol, mkAnnColField colInfo Nothing) + return (fromPGCol pgCol, mkAnnColumnField colInfo Nothing) (ECRel relName mAlias relSelQ) -> do annRel <- convExtRel fieldInfoMap relName mAlias relSelQ sessVarBldr prepValBldr return ( fromRel $ fromMaybe relName mAlias - , either FObj FArr annRel + , either AFObjectRelation AFArrayRelation annRel ) -- let spiT = spiTable selPermInfo @@ -193,11 +193,11 @@ convSelectQ fieldInfoMap selPermInfo selQ sessVarBldr prepValBldr = do let tabFrom = FromTable $ spiTable selPermInfo tabPerm = TablePerm resolvedSelFltr mPermLimit - tabArgs = TableArgs wClause annOrdByM mQueryLimit + tabArgs = SelectArgs wClause annOrdByM mQueryLimit (S.intToSQLExp <$> mQueryOffset) Nothing strfyNum <- stringifyNum <$> askSQLGenCtx - return $ AnnSelG annFlds tabFrom tabPerm tabArgs strfyNum + return $ AnnSelectG annFlds tabFrom tabPerm tabArgs strfyNum where mQueryOffset = sqOffset selQ @@ -224,7 +224,7 @@ convExtRel -> SelectQExt -> SessVarBldr m -> (PGColumnType -> Value -> m S.SQLExp) - -> m (Either ObjSel ArrSel) + -> m (Either ObjectRelationSelect ArraySelect) convExtRel fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do -- Point to the name key relInfo <- withPathK "name" $ @@ -235,9 +235,9 @@ convExtRel fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do case relTy of ObjRel -> do when misused $ throw400 UnexpectedPayload objRelMisuseMsg - return $ Left $ AnnRelG (fromMaybe relName mAlias) colMapping annSel + return $ Left $ AnnRelationSelectG (fromMaybe relName mAlias) colMapping annSel ArrRel -> - return $ Right $ ASSimple $ AnnRelG (fromMaybe relName mAlias) + return $ Right $ ASSimple $ AnnRelationSelectG (fromMaybe relName mAlias) colMapping annSel where pgWhenRelErr = "only relationships can be expanded" @@ -278,9 +278,9 @@ selectQuerySQL :: JsonAggSelect -> AnnSimpleSel -> Q.Query selectQuerySQL jsonAggSelect sel = Q.fromBuilder $ toSQL $ mkSQLSelect jsonAggSelect sel -selectAggQuerySQL :: AnnAggSel -> Q.Query +selectAggQuerySQL :: AnnAggregateSelect -> Q.Query selectAggQuerySQL = - Q.fromBuilder . toSQL . mkAggSelect + Q.fromBuilder . toSQL . mkAggregateSelect asSingleRowJsonResp :: Q.Query -> [Q.PrepArg] -> Q.TxE QErr EncJSON asSingleRowJsonResp query args = diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index cc8a1cca362de..d0da79673ca6c 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -1,16 +1,13 @@ module Hasura.RQL.DML.Select.Internal ( mkSQLSelect - , mkAggSelect - -- , mkConnectionSelect - -- , JoinCandidate(..) + , mkAggregateSelect , mkConnectionSelect , module Hasura.RQL.DML.Select.Types ) where -import Control.Lens hiding (op) +import Control.Lens import Control.Monad.Writer.Strict -import Data.List (delete, sort) import Instances.TH.Lift () import qualified Data.HashMap.Strict as HM @@ -22,7 +19,7 @@ import Hasura.RQL.DML.Internal import Hasura.RQL.DML.Select.Types import Hasura.RQL.GBoolExp import Hasura.RQL.Types -import Hasura.SQL.Rewrite (prefixNumToAliases) +import Hasura.SQL.Rewrite import Hasura.SQL.Types import qualified Hasura.SQL.DML as S @@ -34,8 +31,8 @@ import qualified Hasura.SQL.DML as S functionToIden :: QualifiedFunction -> Iden functionToIden = Iden . qualObjectToText -selFromToFromItem :: Iden -> SelectFrom -> S.FromItem -selFromToFromItem pfx = \case +selectFromToFromItem :: Iden -> SelectFrom -> S.FromItem +selectFromToFromItem pfx = \case FromTable tn -> S.FISimple tn Nothing FromIden i -> S.FIIden i FromFunction qf args defListM -> @@ -44,17 +41,17 @@ selFromToFromItem pfx = \case -- This function shouldn't be present ideally -- You should be able to retrieve this information --- from the FromItem generated with selFromToFromItem +-- from the FromItem generated with selectFromToFromItem -- however given from S.FromItem is modelled, it is not -- possible currently -selFromToQual :: SelectFrom -> S.Qual -selFromToQual = \case +selectFromToQual :: SelectFrom -> S.Qual +selectFromToQual = \case FromTable tn -> S.QualTable tn FromIden i -> S.QualIden i Nothing FromFunction qf _ _ -> S.QualIden (functionToIden qf) Nothing -aggFldToExp :: AggFlds -> S.SQLExp -aggFldToExp aggFlds = jsonRow +aggregateFieldToExp :: AggregateFields -> S.SQLExp +aggregateFieldToExp aggFlds = jsonRow where jsonRow = S.applyJsonBuildObj (concatMap aggToFlds aggFlds) withAls fldName sqlExp = [S.SELit fldName, sqlExp] @@ -63,26 +60,16 @@ aggFldToExp aggFlds = jsonRow AFOp aggOp -> aggOpToObj aggOp AFExp e -> S.SELit e - aggOpToObj (AggOp op flds) = - S.applyJsonBuildObj $ concatMap (colFldsToExtr op) flds + aggOpToObj (AggregateOp opText flds) = + S.applyJsonBuildObj $ concatMap (colFldsToExtr opText) flds - colFldsToExtr op (FieldName t, PCFCol col) = + colFldsToExtr opText (FieldName t, PCFCol col) = [ S.SELit t - , S.SEFnApp op [S.SEIden $ toIden col] Nothing + , S.SEFnApp opText [S.SEIden $ toIden col] Nothing ] colFldsToExtr _ (FieldName t, PCFExp e) = [ S.SELit t , S.SELit e] -arrNodeToSelect :: BaseNode -> [S.Extractor] -> S.BoolExp -> S.Select -arrNodeToSelect bn extrs joinCond = - S.mkSelect - { S.selExtr = extrs - , S.selFrom = Just $ S.FromExp [selFrom] - } - where - selFrom = S.mkSelFromItem (baseNodeToSel joinCond bn) $ S.Alias $ - _bnPrefix bn - asSingleRowExtr :: S.Alias -> S.SQLExp asSingleRowExtr col = S.SEFnApp "coalesce" [jsonAgg, S.SELit "null"] Nothing @@ -93,11 +80,12 @@ asSingleRowExtr col = ] withJsonAggExtr - :: Bool -> Maybe Int -> Maybe S.OrderByExp -> S.Alias -> S.SQLExp -withJsonAggExtr subQueryReq permLimitM ordBy alias = + :: PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Alias -> S.SQLExp +withJsonAggExtr permLimitSubQuery ordBy alias = -- if select has aggregations then use subquery to apply permission limit - if subQueryReq then maybe simpleJsonAgg withPermLimit permLimitM - else simpleJsonAgg + case permLimitSubQuery of + PLSQRequired permLimit -> withPermLimit permLimit + PLSQNotRequired -> simpleJsonAgg where simpleJsonAgg = mkSimpleJsonAgg rowIdenExp ordBy rowIdenExp = S.SEIden $ S.getAlias alias @@ -111,7 +99,7 @@ withJsonAggExtr subQueryReq permLimitM ordBy alias = withPermLimit limit = let subSelect = mkSubSelect limit rowIden = S.mkQIdenExp subSelAls alias - extr = S.Extractor (mkSimpleJsonAgg rowIden newOrdBy) Nothing + extr = S.Extractor (mkSimpleJsonAgg rowIden newOrderBy) Nothing fromExp = S.FromExp $ pure $ S.mkSelFromItem subSelect $ S.Alias subSelAls in S.SESelect $ S.mkSelect { S.selExtr = pure extr @@ -126,7 +114,7 @@ withJsonAggExtr subQueryReq permLimitM ordBy alias = in S.mkSelect { S.selExtr = jsonRowExtr : obExtrs , S.selFrom = Just $ S.FromExp $ pure unnestFromItem , S.selLimit = Just $ S.LimitExp $ S.intToSQLExp limit - , S.selOrderBy = newOrdBy + , S.selOrderBy = newOrderBy } unnestFromItem = @@ -135,11 +123,11 @@ withJsonAggExtr subQueryReq permLimitM ordBy alias = in S.FIUnnest arrayAggItems (S.Alias unnestTable) $ rowIdenExp : map S.SEIden newOBAliases - newOrdBy = bool (Just $ S.OrderByExp newOBItems) Nothing $ null newOBItems + newOrderBy = S.OrderByExp <$> NE.nonEmpty newOBItems - (newOBItems, obCols, newOBAliases) = maybe ([], [], []) transformOrdBy ordBy - transformOrdBy (S.OrderByExp l) = unzip3 $ - flip map (zip l [1..]) $ \(obItem, i::Int) -> + (newOBItems, obCols, newOBAliases) = maybe ([], [], []) transformOrderBy ordBy + transformOrderBy (S.OrderByExp l) = unzip3 $ + flip map (zip (toList l) [1..]) $ \(obItem, i::Int) -> let iden = Iden $ "ob_col_" <> T.pack (show i) in ( obItem{S.oColumn = S.SEIden iden} , S.oColumn obItem @@ -147,112 +135,81 @@ withJsonAggExtr subQueryReq permLimitM ordBy alias = ) asJsonAggExtr - :: JsonAggSelect -> S.Alias -> Bool -> Maybe Int -> Maybe S.OrderByExp -> S.Extractor -asJsonAggExtr jsonAggSelect als subQueryReq permLimit ordByExpM = + :: JsonAggSelect -> S.Alias -> PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Extractor +asJsonAggExtr jsonAggSelect als permLimitSubQuery ordByExpM = flip S.Extractor (Just als) $ case jsonAggSelect of - JASMultipleRows -> withJsonAggExtr subQueryReq permLimit ordByExpM als + JASMultipleRows -> withJsonAggExtr permLimitSubQuery ordByExpM als JASSingleObject -> asSingleRowExtr als -- array relationships are not grouped, so have to be prefixed by -- parent's alias -mkUniqArrRelAls :: FieldName -> [FieldName] -> Iden -mkUniqArrRelAls parAls flds = - Iden $ - getFieldNameTxt parAls <> "." - <> T.intercalate "." (map getFieldNameTxt flds) - -mkArrRelTableAls :: Iden -> FieldName -> [FieldName] -> Iden -mkArrRelTableAls pfx parAls flds = +mkUniqArrayRelationAlias :: FieldName -> [FieldName] -> Iden +mkUniqArrayRelationAlias parAls flds = + let sortedFields = sort flds + in Iden $ + getFieldNameTxt parAls <> "." + <> T.intercalate "." (map getFieldNameTxt sortedFields) + +mkArrayRelationTableAlias :: Iden -> FieldName -> [FieldName] -> Iden +mkArrayRelationTableAlias pfx parAls flds = pfx <> Iden ".ar." <> uniqArrRelAls where - uniqArrRelAls = mkUniqArrRelAls parAls flds + uniqArrRelAls = mkUniqArrayRelationAlias parAls flds -mkObjRelTableAls :: Iden -> RelName -> Iden -mkObjRelTableAls pfx relName = +mkObjectRelationTableAlias :: Iden -> RelName -> Iden +mkObjectRelationTableAlias pfx relName = pfx <> Iden ".or." <> toIden relName -mkComputedFieldTableAls :: Iden -> FieldName -> Iden -mkComputedFieldTableAls pfx fldAls = +mkComputedFieldTableAlias :: Iden -> FieldName -> Iden +mkComputedFieldTableAlias pfx fldAls = pfx <> Iden ".cf." <> toIden fldAls -mkBaseTableAls :: Iden -> Iden -mkBaseTableAls pfx = +mkBaseTableAlias :: Iden -> Iden +mkBaseTableAlias pfx = pfx <> Iden ".base" -mkBaseTableColAls :: Iden -> PGCol -> Iden -mkBaseTableColAls pfx pgColumn = +mkBaseTableColumnAlias :: Iden -> PGCol -> Iden +mkBaseTableColumnAlias pfx pgColumn = pfx <> Iden ".pg." <> toIden pgColumn mkOrderByFieldName :: RelName -> FieldName mkOrderByFieldName relName = FieldName $ relNameToTxt relName <> "." <> "order_by" +mkAggregateOrderByAlias :: AnnAggregateOrderBy -> S.Alias +mkAggregateOrderByAlias = (S.Alias . Iden) . \case + AAOCount -> "count" + AAOOp opText col -> opText <> "." <> getPGColTxt (pgiColumn col) + +mkArrayRelationSourcePrefix + :: Iden + -> FieldName + -> HM.HashMap FieldName [FieldName] + -> FieldName + -> Iden +mkArrayRelationSourcePrefix parentSourcePrefix parentFieldName similarFieldsMap fieldName = + mkArrayRelationTableAlias parentSourcePrefix parentFieldName $ + HM.lookupDefault [fieldName] fieldName similarFieldsMap + +mkArrayRelationAlias + :: FieldName + -> HM.HashMap FieldName [FieldName] + -> FieldName + -> S.Alias +mkArrayRelationAlias parentFieldName similarFieldsMap fieldName = + S.Alias $ mkUniqArrayRelationAlias parentFieldName $ + HM.lookupDefault [fieldName] fieldName similarFieldsMap + fromTableRowArgs :: Iden -> FunctionArgsExpTableRow S.SQLExp -> S.FunctionArgs fromTableRowArgs pfx = toFunctionArgs . fmap toSQLExp where toFunctionArgs (FunctionArgsExp positional named) = S.FunctionArgs positional named - toSQLExp (AETableRow Nothing) = S.SERowIden $ mkBaseTableAls pfx - toSQLExp (AETableRow (Just acc)) = S.mkQIdenExp (mkBaseTableAls pfx) acc + toSQLExp (AETableRow Nothing) = S.SERowIden $ mkBaseTableAlias pfx + toSQLExp (AETableRow (Just acc)) = S.mkQIdenExp (mkBaseTableAlias pfx) acc toSQLExp (AEInput s) = s --- posttgres ignores anything beyond 63 chars for an iden --- in this case, we'll need to use json_build_object function --- json_build_object is slower than row_to_json hence it is only --- used when needed -buildJsonObject - :: Iden -> FieldName -> ArrRelCtx -> Bool - -> [(FieldName, AnnFld)] -> (S.Alias, S.SQLExp) -buildJsonObject pfx parAls arrRelCtx strfyNum flds = - if any ( (> 63) . T.length . getFieldNameTxt . fst ) flds - then withJsonBuildObj parAls jsonBuildObjExps - else withRowToJSON parAls rowToJsonExtrs - where - jsonBuildObjExps = concatMap (toSQLFld withAlsExp) flds - rowToJsonExtrs = map (toSQLFld withAlsExtr) flds - - withAlsExp fldName sqlExp = - [S.SELit $ getFieldNameTxt fldName, sqlExp] - - withAlsExtr fldName sqlExp = - S.Extractor sqlExp $ Just $ S.toAlias fldName - - toSQLFld :: (FieldName -> S.SQLExp -> f) - -> (FieldName, AnnFld) -> f - toSQLFld f (fldAls, fld) = f fldAls $ case fld of - FCol c -> toSQLCol c - FExp e -> S.SELit e - FObj objSel -> - let qual = mkObjRelTableAls pfx $ aarName objSel - in S.mkQIdenExp qual fldAls - FArr arrSel -> - let arrPfx = _aniPrefix $ mkArrNodeInfo pfx parAls arrRelCtx $ - ANIField (fldAls, arrSel) - in S.mkQIdenExp arrPfx fldAls - FComputedField (CFSScalar computedFieldScalar) -> - fromScalarComputedField computedFieldScalar - FComputedField (CFSTable _ _) -> - let ccPfx = mkComputedFieldTableAls pfx fldAls - in S.mkQIdenExp ccPfx fldAls - - toSQLCol :: AnnColField -> S.SQLExp - toSQLCol (AnnColField col asText colOpM) = - toJSONableExp strfyNum (pgiType col) asText $ withColOp colOpM $ - S.mkQIdenExp (mkBaseTableAls pfx) $ pgiColumn col - - fromScalarComputedField :: ComputedFieldScalarSel S.SQLExp -> S.SQLExp - fromScalarComputedField computedFieldScalar = - toJSONableExp strfyNum (PGColumnScalar ty) False $ withColOp colOpM $ - S.SEFunction $ S.FunctionExp fn (fromTableRowArgs pfx args) Nothing - where - ComputedFieldScalarSel fn args ty colOpM = computedFieldScalar - - withColOp :: Maybe ColOp -> S.SQLExp -> S.SQLExp - withColOp colOpM sqlExp = case colOpM of - Nothing -> sqlExp - Just (ColOp op cExp) -> S.mkSQLOpExp op sqlExp cExp - -- uses row_to_json to build a json object withRowToJSON :: FieldName -> [S.Extractor] -> (S.Alias, S.SQLExp) @@ -269,440 +226,575 @@ withJsonBuildObj parAls exps = where jsonRow = S.applyJsonBuildObj exps -mkAggObFld :: AnnAggOrdBy -> FieldName -mkAggObFld = \case - AAOCount -> FieldName "count" - AAOOp op col -> FieldName $ op <> "." <> getPGColTxt col - -mkAggObExtrAndFlds :: AnnAggOrdBy -> (S.Extractor, AggFlds) -mkAggObExtrAndFlds annAggOb = case annAggOb of - AAOCount -> - ( S.Extractor S.countStar als - , [(FieldName "count", AFCount S.CTStar)] - ) - AAOOp op pgColumn -> - ( S.Extractor (S.SEFnApp op [S.SEIden $ toIden pgColumn] Nothing) als - , [(FieldName op, AFOp $ AggOp op [(fromPGCol pgColumn, PCFCol pgColumn)])] - ) +mkAggregateOrderByExtractorAndFields + :: AnnAggregateOrderBy -> (S.Extractor, AggregateFields) +mkAggregateOrderByExtractorAndFields annAggOrderBy = + case annAggOrderBy of + AAOCount -> + ( S.Extractor S.countStar alias + , [(FieldName "count", AFCount S.CTStar)] + ) + AAOOp opText pgColumnInfo -> + let pgColumn = pgiColumn pgColumnInfo + in ( S.Extractor (S.SEFnApp opText [S.SEIden $ toIden pgColumn] Nothing) alias + , [(FieldName opText, AFOp $ AggregateOp opText [(fromPGCol pgColumn, PCFCol pgColumn)])] + ) where - als = Just $ S.toAlias $ mkAggObFld annAggOb - -processAnnOrderByItem - :: Iden - -> FieldName - -> ArrRelCtx - -> Bool - -> AnnOrderByItem - -- the extractors which will select the needed columns - -> ( (S.Alias, S.SQLExp) - -- the sql order by item that is attached to the final select - , S.OrderByItem - -- extra nodes for order by - , OrderByNode - ) -processAnnOrderByItem pfx parAls arrRelCtx strfyNum obItemG = - ( (obColAls, obColExp) - , sqlOrdByItem - , relNodeM - ) - where - OrderByItemG obTyM annObCol obNullsM = obItemG - ((obColAls, obColExp), relNodeM) = - processAnnOrderByCol pfx parAls arrRelCtx strfyNum annObCol - - sqlOrdByItem = - S.OrderByItem (S.SEIden $ toIden obColAls) - (unOrderType <$> obTyM) (unNullsOrder <$> obNullsM) - -processAnnOrderByCol - :: Iden - -> FieldName - -> ArrRelCtx - -> Bool - -> AnnObCol - -- the extractors which will select the needed columns - -> ( (S.Alias, S.SQLExp) - -- extra nodes for order by - , OrderByNode - ) -processAnnOrderByCol pfx parAls arrRelCtx strfyNum = \case - AOCPG pgColumn -> - let - qualCol = S.mkQIdenExp (mkBaseTableAls pfx) (toIden pgColumn) - obColAls = mkBaseTableColAls pfx pgColumn - in ( (S.Alias obColAls, qualCol) - , OBNNothing - ) + alias = Just $ mkAggregateOrderByAlias annAggOrderBy + +mkAnnOrderByAlias + :: Iden -> FieldName -> SimilarArrayFields -> AnnOrderByElementG v -> S.Alias +mkAnnOrderByAlias pfx parAls similarFields = \case + AOCColumn pgColumnInfo -> + let pgColumn = pgiColumn pgColumnInfo + obColAls = mkBaseTableColumnAlias pfx pgColumn + in S.Alias obColAls -- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest" - AOCObj (RelInfo rn _ colMapping relTab _) relFltr rest -> - let relPfx = mkObjRelTableAls pfx rn + AOCObjectRelation relInfo _ rest -> + let rn = riName relInfo + relPfx = mkObjectRelationTableAlias pfx rn ordByFldName = mkOrderByFieldName rn - ((nesAls, nesCol), ordByNode) = - processAnnOrderByCol relPfx ordByFldName emptyArrRelCtx strfyNum rest - (objNodeM, arrNodeM) = case ordByNode of - OBNNothing -> (Nothing, Nothing) - OBNObjNode name node -> (Just (name, node), Nothing) - OBNArrNode als node -> (Nothing, Just (als, node)) - qualCol = S.mkQIdenExp relPfx nesAls - relBaseNode = - BaseNode relPfx Nothing (S.FISimple relTab Nothing) - (toSQLBoolExp (S.QualTable relTab) relFltr) - Nothing Nothing Nothing - (HM.singleton nesAls nesCol) - (maybe HM.empty (uncurry HM.singleton) objNodeM) - (maybe HM.empty (uncurry HM.singleton) arrNodeM) - HM.empty - relNode = ObjNode colMapping relBaseNode - in ( (nesAls, qualCol) - , OBNObjNode rn relNode - ) - AOCAgg (RelInfo rn _ colMapping relTab _ ) relFltr annAggOb -> - let ArrNodeInfo arrAls arrPfx _ = - mkArrNodeInfo pfx parAls arrRelCtx $ ANIAggOrdBy rn - fldName = mkAggObFld annAggOb - qOrdBy = S.mkQIdenExp arrPfx $ toIden fldName - tabFrom = FromTable relTab - tabPerm = TablePerm relFltr Nothing - (extr, arrFlds) = mkAggObExtrAndFlds annAggOb - selFld = TAFAgg arrFlds - bn = mkBaseNode False (Prefixes arrPfx pfx) fldName selFld tabFrom - tabPerm noTableArgs Nothing strfyNum - aggNode = ArrNode [extr] colMapping $ mergeBaseNodes bn $ - mkEmptyBaseNode arrPfx tabFrom - obAls = arrPfx <> Iden "." <> toIden fldName - in ( (S.Alias obAls, qOrdBy) - , OBNArrNode arrAls aggNode - ) - -processDistinctOnCol + nesAls = mkAnnOrderByAlias relPfx ordByFldName mempty rest + in nesAls + AOCArrayAggregation relInfo _ aggOrderBy -> + let rn = riName relInfo + arrPfx = mkArrayRelationSourcePrefix pfx parAls similarFields $ + mkOrderByFieldName rn + obAls = arrPfx <> Iden "." <> toIden (mkAggregateOrderByAlias aggOrderBy) + in S.Alias obAls + +processDistinctOnColumns :: Iden -> NE.NonEmpty PGCol -> ( S.DistinctExpr - -- additional column extractors - , [(S.Alias, S.SQLExp)] + , [(S.Alias, S.SQLExp)] -- additional column extractors ) -processDistinctOnCol pfx neCols = (distOnExp, colExtrs) +processDistinctOnColumns pfx neCols = (distOnExp, colExtrs) where cols = toList neCols distOnExp = S.DistinctOn $ map (S.SEIden . toIden . mkQColAls) cols - mkQCol c = S.mkQIdenExp (mkBaseTableAls pfx) $ toIden c - mkQColAls = S.Alias . mkBaseTableColAls pfx + mkQCol c = S.mkQIdenExp (mkBaseTableAlias pfx) $ toIden c + mkQColAls = S.Alias . mkBaseTableColumnAlias pfx colExtrs = flip map cols $ mkQColAls &&& mkQCol +type SimilarArrayFields = HM.HashMap FieldName [FieldName] -mkEmptyBaseNode :: Iden -> SelectFrom -> BaseNode -mkEmptyBaseNode pfx selectFrom = - BaseNode pfx Nothing fromItem (S.BELit True) Nothing Nothing - Nothing selOne HM.empty HM.empty HM.empty +mkSimilarArrayFields + :: Eq v + => AnnFieldsG v + -> Maybe (NE.NonEmpty (AnnOrderByItemG v)) + -> SimilarArrayFields +mkSimilarArrayFields annFields maybeOrderBys = + HM.fromList $ flip map allTuples $ + \(relNameAndArgs, fieldName) -> (fieldName, getSimilarFields relNameAndArgs) where - selOne = HM.singleton (S.Alias $ pfx <> Iden "__one") (S.SEUnsafe "1") - fromItem = selFromToFromItem pfx selectFrom - -aggSelToArrNode :: Prefixes -> FieldName -> ArrRelAgg -> ArrNode -aggSelToArrNode pfxs als aggSel = - ArrNode [extr] colMapping mergedBN + getSimilarFields relNameAndArgs = map snd $ filter ((== relNameAndArgs) . fst) allTuples + allTuples = arrayRelationTuples <> aggOrderByRelationTuples + arrayRelationTuples = + let arrayFields = mapMaybe getAnnArr annFields + in flip map arrayFields $ + \(f, relSel) -> (getArrayRelNameAndSelectArgs relSel, f) + + aggOrderByRelationTuples = + let mkItem (relName, fieldName) = ( (relName, noSelectArgs) + , fieldName + ) + in map mkItem $ maybe [] + (mapMaybe (fetchAggOrderByRels . obiColumn) . toList) maybeOrderBys + + fetchAggOrderByRels (AOCArrayAggregation ri _ _) = + Just (riName ri, mkOrderByFieldName $ riName ri) + fetchAggOrderByRels _ = Nothing + +getArrayRelNameAndSelectArgs :: ArraySelectG v -> (RelName, SelectArgsG v) +getArrayRelNameAndSelectArgs = \case + ASSimple r -> (aarRelationshipName r, _asnArgs $ aarAnnSelect r) + ASAggregate r -> (aarRelationshipName r, _asnArgs $ aarAnnSelect r) + ASConnection r -> (aarRelationshipName r, _asnArgs $ _csSelect $ aarAnnSelect r) + +getAnnArr :: (a, AnnFieldG v) -> Maybe (a, ArraySelectG v) +getAnnArr (f, annFld) = case annFld of + AFArrayRelation ar -> Just (f, ar) + _ -> Nothing + + +withWriteJoinTree + :: (MonadWriter JoinTree m) + => (JoinTree -> b -> JoinTree) + -> m (a, b) + -> m a +withWriteJoinTree joinTreeUpdater action = + pass $ do + (out, result) <- action + let fromJoinTree joinTree = + joinTreeUpdater joinTree result + pure (out, fromJoinTree) + +withWriteObjectRelation + :: (MonadWriter JoinTree m) + => m ( ObjectRelationSource + , HM.HashMap S.Alias S.SQLExp + , a + ) + -> m a +withWriteObjectRelation action = + withWriteJoinTree updateJoinTree $ do + (source, nodeExtractors, out) <- action + pure (out, (source, nodeExtractors)) where - AnnRelG _ colMapping annSel = aggSel - AnnSelG aggFlds tabFrm tabPerm tabArgs strfyNum = annSel - fldAls = S.Alias $ toIden als - - extr = flip S.Extractor (Just fldAls) $ S.applyJsonBuildObj $ - concatMap selFldToExtr aggFlds - - permLimit = _tpLimit tabPerm - ordBy = _bnOrderBy mergedBN - - allBNs = map mkAggBaseNode aggFlds - emptyBN = mkEmptyBaseNode (_pfThis pfxs) tabFrm - mergedBN = foldr mergeBaseNodes emptyBN allBNs - - mkAggBaseNode (fn, selFld) = - mkBaseNode subQueryReq pfxs fn selFld tabFrm tabPerm tabArgs Nothing strfyNum - - selFldToExtr (FieldName t, fld) = (:) (S.SELit t) $ pure $ case fld of - TAFAgg flds -> aggFldToExp flds - TAFNodes _ -> - withJsonAggExtr subQueryReq permLimit ordBy $ S.Alias $ Iden t - TAFExp e -> - -- bool_or to force aggregation - S.SEFnApp "coalesce" - [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing - - subQueryReq = hasAggFld aggFlds - -hasAggFld :: Foldable t => t (a, TableAggFldG v) -> Bool -hasAggFld = any (isTabAggFld . snd) + updateJoinTree joinTree (source, nodeExtractors) = + let selectNode = SelectNode nodeExtractors joinTree + in mempty{_jtObjectRelations = HM.singleton source selectNode} + +withWriteArrayRelation + :: (MonadWriter JoinTree m) + => m ( ArrayRelationSource + , S.Extractor + , HM.HashMap S.Alias S.SQLExp + , a + ) + -> m a +withWriteArrayRelation action = + withWriteJoinTree updateJoinTree $ do + (source, topExtractor, nodeExtractors, out) <- action + pure (out, (source, topExtractor, nodeExtractors)) where - isTabAggFld (TAFAgg _) = True - isTabAggFld _ = False - -mkArrNodeInfo - :: Iden - -> FieldName - -> ArrRelCtx - -> ArrNodeItem - -> ArrNodeInfo -mkArrNodeInfo pfx parAls (ArrRelCtx arrFlds obRels) = \case - ANIField aggFld@(fld, annArrSel) -> - let (rn, tabArgs) = fetchRNAndTArgs annArrSel - similarFlds = getSimilarAggFlds rn tabArgs $ delete aggFld - similarFldNames = map fst similarFlds - similarOrdByFound = rn `elem` obRels && tabArgs == noTableArgs - ordByFldName = mkOrderByFieldName rn - extraOrdByFlds = bool [] [ordByFldName] similarOrdByFound - sortedFlds = sort $ fld : (similarFldNames <> extraOrdByFlds) - alias = S.Alias $ mkUniqArrRelAls parAls sortedFlds - prefix = mkArrRelTableAls pfx parAls sortedFlds - in ArrNodeInfo alias prefix $ - subQueryRequired similarFlds similarOrdByFound - ANIAggOrdBy rn -> - let similarFlds = map fst $ getSimilarAggFlds rn noTableArgs id - ordByFldName = mkOrderByFieldName rn - sortedFlds = sort $ ordByFldName:similarFlds - alias = S.Alias $ mkUniqArrRelAls parAls sortedFlds - prefix = mkArrRelTableAls pfx parAls sortedFlds - in ArrNodeInfo alias prefix False + updateJoinTree joinTree (source, topExtractor, nodeExtractors) = + let arraySelectNode = ArraySelectNode [topExtractor] $ + SelectNode nodeExtractors joinTree + in mempty{_jtArrayRelations = HM.singleton source arraySelectNode} + +withWriteArrayConnection + :: (MonadWriter JoinTree m) + => m ( ArrayConnectionSource + , S.Extractor + , HM.HashMap S.Alias S.SQLExp + , a + ) + -> m a +withWriteArrayConnection action = + withWriteJoinTree updateJoinTree $ do + (source, topExtractor, nodeExtractors, out) <- action + pure (out, (source, topExtractor, nodeExtractors)) where - getSimilarAggFlds rn tabArgs f = - flip filter (f arrFlds) $ \(_, annArrSel) -> - let (lrn, lTabArgs) = fetchRNAndTArgs annArrSel - in (lrn == rn) && (lTabArgs == tabArgs) - - subQueryRequired similarFlds hasSimOrdBy = - hasSimOrdBy || any hasAgg similarFlds - - hasAgg (_, ASSimple _) = False - hasAgg (_, ASAgg (AnnRelG _ _ annSel)) = hasAggFld $ _asnFields annSel - - fetchRNAndTArgs (ASSimple (AnnRelG rn _ annSel)) = - (rn, _asnArgs annSel) - fetchRNAndTArgs (ASAgg (AnnRelG rn _ annSel)) = - (rn, _asnArgs annSel) - -fetchOrdByAggRels - :: Maybe (NE.NonEmpty AnnOrderByItem) - -> [RelName] -fetchOrdByAggRels orderByM = fromMaybe [] relNamesM + updateJoinTree joinTree (source, topExtractor, nodeExtractors) = + let arraySelectNode = ArraySelectNode [topExtractor] $ + SelectNode nodeExtractors joinTree + in mempty{_jtArrayConnections = HM.singleton source arraySelectNode} + +withWriteComputedFieldTableSet + :: (MonadWriter JoinTree m) + => m ( ComputedFieldTableSetSource + , HM.HashMap S.Alias S.SQLExp + , a + ) + -> m a +withWriteComputedFieldTableSet action = + withWriteJoinTree updateJoinTree $ do + (source, nodeExtractors, out) <- action + pure (out, (source, nodeExtractors)) where - relNamesM = - mapMaybe (fetchAggOrdByRels . obiColumn) . toList <$> orderByM + updateJoinTree joinTree (source, nodeExtractors) = + let selectNode = SelectNode nodeExtractors joinTree + in mempty{_jtComputedFieldTableSets = HM.singleton source selectNode} - fetchAggOrdByRels (AOCAgg ri _ _) = Just $ riName ri - fetchAggOrdByRels _ = Nothing -mkOrdByItems - :: Iden -> FieldName - -> Maybe (NE.NonEmpty AnnOrderByItem) - -> Bool - -> ArrRelCtx - -- extractors - -> ( [(S.Alias, S.SQLExp)] - -- object relation nodes - , HM.HashMap RelName ObjNode - -- array relation aggregate nodes - , HM.HashMap S.Alias ArrNode - -- final order by expression - , Maybe S.OrderByExp - ) -mkOrdByItems pfx fldAls orderByM strfyNum arrRelCtx = - (obExtrs, ordByObjsMap, ordByArrsMap, ordByExpM) +processAnnSimpleSelect + :: forall m. ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => SourcePrefixes + -> FieldName + -> PermissionLimitSubQuery + -> AnnSimpleSel + -> m ( SelectSource + , HM.HashMap S.Alias S.SQLExp + ) +processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery annSimpleSel = do + (selectSource, orderByAndDistinctExtrs, _) <- + processSelectParams sourcePrefixes fieldAlias similarArrayFields tableFrom + permLimitSubQuery tablePermissions tableArgs + annFieldsExtr <- processAnnFields (_pfThis sourcePrefixes) fieldAlias similarArrayFields annSelFields + let allExtractors = HM.fromList $ annFieldsExtr : orderByAndDistinctExtrs + pure (selectSource, allExtractors) where - procAnnOrdBy' = processAnnOrderByItem pfx fldAls arrRelCtx strfyNum - procOrdByM = - unzip3 . map procAnnOrdBy' . toList <$> orderByM - - obExtrs = maybe [] (^. _1) procOrdByM - ordByExpM = S.OrderByExp . (^. _2) <$> procOrdByM - - ordByObjs = mapMaybe getOrdByRelNode $ maybe [] (^. _3) procOrdByM - ordByObjsMap = HM.fromListWith mergeObjNodes ordByObjs - - ordByAggArrs = mapMaybe getOrdByAggNode $ maybe [] (^. _3) procOrdByM - ordByArrsMap = HM.fromListWith mergeArrNodes ordByAggArrs - - getOrdByRelNode (OBNObjNode name node) = Just (name, node) - getOrdByRelNode _ = Nothing - - getOrdByAggNode (OBNArrNode als node) = Just (als, node) - getOrdByAggNode _ = Nothing - -cursorAlias :: S.Alias -cursorAlias = S.Alias $ Iden "__cursor" + AnnSelectG annSelFields tableFrom tablePermissions tableArgs _ = annSimpleSel + similarArrayFields = + mkSimilarArrayFields annSelFields $ _saOrderBy tableArgs + +processAnnAggregateSelect + :: forall m. ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => SourcePrefixes + -> FieldName + -> AnnAggregateSelect + -> m ( SelectSource + , HM.HashMap S.Alias S.SQLExp + , S.Extractor + ) +processAnnAggregateSelect sourcePrefixes fieldAlias annAggSel = do + (selectSource, orderByAndDistinctExtrs, _) <- + processSelectParams sourcePrefixes fieldAlias similarArrayFields tableFrom + permLimitSubQuery tablePermissions tableArgs + let thisSourcePrefix = _pfThis sourcePrefixes + processedFields <- forM aggSelFields $ \(fieldName, field) -> + (fieldName,) <$> + case field of + TAFAgg aggFields -> + pure ( aggregateFieldsToExtractorExps thisSourcePrefix aggFields + , aggregateFieldToExp aggFields + ) + TAFNodes annFields -> do + annFieldExtr <- processAnnFields thisSourcePrefix fieldName similarArrayFields annFields + pure ( [annFieldExtr] + , withJsonAggExtr permLimitSubQuery (_ssOrderBy selectSource) $ + S.Alias $ toIden fieldName + ) + TAFExp e -> + pure ( [] + -- bool_or to force aggregation + , S.SEFnApp "coalesce" + [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing + ) -mkCursorExtractor :: Iden -> [PGCol] -> S.SQLExp -mkCursorExtractor pfx columns = - flip S.SETyAnn S.textTypeAnn $ - S.applyJsonBuildObj $ flip concatMap columns $ - \column -> [ S.SELit $ getPGColTxt column - , S.mkQIdenExp (mkBaseTableAls pfx) column - ] + let topLevelExtractor = + flip S.Extractor (Just $ S.Alias $ toIden fieldAlias) $ + S.applyJsonBuildObj $ flip concatMap (map (second snd) processedFields) $ + \(FieldName fieldText, fieldExp) -> [S.SELit fieldText, fieldExp] + nodeExtractors = HM.fromList $ + concatMap (fst . snd) processedFields <> orderByAndDistinctExtrs -mkBaseNode - :: Bool - -> Prefixes + pure (selectSource, nodeExtractors, topLevelExtractor) + where + AnnSelectG aggSelFields tableFrom tablePermissions tableArgs _ = annAggSel + permLimit = _tpLimit tablePermissions + orderBy = _saOrderBy tableArgs + permLimitSubQuery = mkPermissionLimitSubQuery permLimit aggSelFields orderBy + similarArrayFields = HM.unions $ + flip map (map snd aggSelFields) $ \case + TAFAgg _ -> mempty + TAFNodes annFlds -> + mkSimilarArrayFields annFlds orderBy + TAFExp _ -> mempty + +mkPermissionLimitSubQuery + :: Maybe Int + -> TableAggregateFields + -> Maybe (NE.NonEmpty AnnOrderByItem) + -> PermissionLimitSubQuery +mkPermissionLimitSubQuery permLimit aggFields orderBys = + case permLimit of + Nothing -> PLSQNotRequired + Just limit -> + if hasAggregateField || hasAggOrderBy then PLSQRequired limit + else PLSQNotRequired + where + hasAggregateField = flip any (map snd aggFields) $ + \case + TAFAgg _ -> True + _ -> False + + hasAggOrderBy = case orderBys of + Nothing -> False + Just l -> flip any (concatMap toList $ toList l) $ + \case + AOCArrayAggregation{} -> True + _ -> False + +processArrayRelation + :: forall m. ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => SourcePrefixes -> FieldName - -> TableAggFld + -> S.Alias + -> ArraySelect + -> m () +processArrayRelation sourcePrefixes fieldAlias relAlias arrSel = + case arrSel of + ASSimple annArrRel -> withWriteArrayRelation $ do + let AnnRelationSelectG _ colMapping sel = annArrRel + permLimitSubQuery = + maybe PLSQNotRequired PLSQRequired $ _tpLimit $ _asnPerm sel + (source, nodeExtractors) <- + processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery sel + let topExtr = asJsonAggExtr JASMultipleRows (S.toAlias fieldAlias) + permLimitSubQuery $ _ssOrderBy source + pure ( ArrayRelationSource relAlias colMapping source + , topExtr + , nodeExtractors + , () + ) + ASAggregate aggSel -> withWriteArrayRelation $ do + let AnnRelationSelectG _ colMapping sel = aggSel + (source, nodeExtractors, topExtr) <- + processAnnAggregateSelect sourcePrefixes fieldAlias sel + pure ( ArrayRelationSource relAlias colMapping source + , topExtr + , nodeExtractors + , () + ) + ASConnection connSel -> withWriteArrayConnection $ do + let AnnRelationSelectG _ colMapping sel = connSel + (source, topExtractor, nodeExtractors) <- + processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping sel + pure ( source + , topExtractor + , nodeExtractors + , () + ) + +processSelectParams + :: forall m. ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => SourcePrefixes + -> FieldName + -> SimilarArrayFields -> SelectFrom + -> PermissionLimitSubQuery -> TablePerm - -> TableArgs - -> Maybe (NonEmpty PGCol) - -> Bool - -> BaseNode -mkBaseNode subQueryReq pfxs fldAls annSelFlds selectFrom - tablePerm tableArgs primaryKeyColumns strfyNum = - - BaseNode thisPfx distExprM fromItem finalWhere ordByExpM finalLimit offsetM - allExtrs allObjsWithOb allArrsWithOb computedFields + -> SelectArgs + -> m ( SelectSource + , [(S.Alias, S.SQLExp)] + , Maybe S.SQLExp -- Order by cursor + ) +processSelectParams sourcePrefixes fieldAlias similarArrFields selectFrom + permLimitSubQ tablePermissions tableArgs = do + maybeOrderBy <- mapM + (processOrderByItems thisSourcePrefix fieldAlias similarArrFields) + orderByM + let fromItem = selectFromToFromItem (_pfBase sourcePrefixes) selectFrom + (maybeDistinct, distinctExtrs) = + maybe (Nothing, []) (first Just) $ processDistinctOnColumns thisSourcePrefix <$> distM + finalWhere = toSQLBoolExp (selectFromToQual selectFrom) $ + maybe permFilter (andAnnBoolExps permFilter) whereM + selectSource = SelectSource thisSourcePrefix fromItem maybeDistinct finalWhere + ((^. _2) <$> maybeOrderBy) finalLimit offsetM + orderByExtrs = maybe [] (^. _1) maybeOrderBy + pure ( selectSource + , orderByExtrs <> distinctExtrs + , (^. _3) <$> maybeOrderBy + ) where - Prefixes thisPfx baseTablepfx = pfxs - TablePerm permFilter permLimit = tablePerm - TableArgs whereM orderByM inpLimitM offsetM distM = tableArgs - - -- if sub query is used, then only use input limit + thisSourcePrefix = _pfThis sourcePrefixes + SelectArgs whereM orderByM inpLimitM offsetM distM = tableArgs + TablePerm permFilter permLimit = tablePermissions + finalLimit = + -- if sub query is required, then only use input limit -- because permission limit is being applied in subquery -- else compare input and permission limits - finalLimit = - if subQueryReq then inpLimitM - else withPermLimit + case permLimitSubQ of + PLSQRequired _ -> inpLimitM + PLSQNotRequired -> compareLimits - withPermLimit = + compareLimits = case (inpLimitM, permLimit) of (inpLim, Nothing) -> inpLim (Nothing, permLim) -> permLim (Just inp, Just perm) -> Just $ if inp < perm then inp else perm - - aggOrdByRelNames = fetchOrdByAggRels orderByM - - (allExtrs, allObjsWithOb, allArrsWithOb, computedFields, ordByExpM) = - case annSelFlds of - TAFNodes flds -> - let arrFlds = mapMaybe getAnnArr flds - arrRelCtx = mkArrRelCtx arrFlds - selExtr = buildJsonObject thisPfx fldAls arrRelCtx strfyNum flds - cursorExtrs = maybe [] (pure . (cursorAlias,) . mkCursorExtractor thisPfx . toList) - primaryKeyColumns - -- all object relationships - objNodes = HM.fromListWith mergeObjNodes $ - map mkObjItem (mapMaybe getAnnObj flds) - -- all array items (array relationships + aggregates) - arrNodes = HM.fromListWith mergeArrNodes $ - map (mkArrItem arrRelCtx) arrFlds - -- all computed fields with table returns - computedFieldNodes = HM.fromList $ map mkComputedFieldTable $ - mapMaybe getComputedFieldTable flds - - (obExtrs, ordByObjs, ordByArrs, obeM) - = mkOrdByItems' arrRelCtx - allObjs = HM.unionWith mergeObjNodes objNodes ordByObjs - allArrs = HM.unionWith mergeArrNodes arrNodes ordByArrs - - in ( HM.fromList $ selExtr:obExtrs <> distExtrs <> cursorExtrs - , allObjs - , allArrs - , computedFieldNodes - , obeM - ) - TAFAgg tabAggs -> - let extrs = concatMap (fetchExtrFromAggFld . snd) tabAggs - (obExtrs, ordByObjs, ordByArrs, obeM) - = mkOrdByItems' emptyArrRelCtx - in ( HM.fromList $ extrs <> obExtrs <> distExtrs - , ordByObjs - , ordByArrs - , HM.empty - , obeM - ) - TAFExp _ -> - let (obExtrs, ordByObjs, ordByArrs, obeM) - = mkOrdByItems' emptyArrRelCtx - in (HM.fromList obExtrs, ordByObjs, ordByArrs, HM.empty, obeM) - - fetchExtrFromAggFld (AFCount cty) = countTyToExps cty - fetchExtrFromAggFld (AFOp aggOp) = aggOpToExps aggOp - fetchExtrFromAggFld (AFExp _) = [] - - countTyToExps S.CTStar = [] - countTyToExps (S.CTSimple cols) = colsToExps cols - countTyToExps (S.CTDistinct cols) = colsToExps cols - +processOrderByItems + :: forall m. ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => Iden + -> FieldName + -> SimilarArrayFields + -> NE.NonEmpty AnnOrderByItem + -> m ( [(S.Alias, S.SQLExp)] -- Order by Extractors + , S.OrderByExp + , S.SQLExp -- The cursor expression + ) +processOrderByItems sourcePrefix' fieldAlias' similarArrayFields orderByItems = do + orderByItemExps <- forM orderByItems processAnnOrderByItem + let orderByExp = S.OrderByExp $ toOrderByExp <$> orderByItemExps + orderByExtractors = concat $ toList $ map snd . toList <$> orderByItemExps + cursor = mkCursorExp $ toList orderByItemExps + pure (orderByExtractors, orderByExp, cursor) + where + processAnnOrderByItem :: AnnOrderByItem -> m OrderByItemExp + processAnnOrderByItem orderByItem = + forM orderByItem $ \ordByCol -> (ordByCol,) <$> + processAnnOrderByElement sourcePrefix' fieldAlias' ordByCol + + processAnnOrderByElement + :: Iden -> FieldName -> AnnOrderByElement S.SQLExp -> m (S.Alias, S.SQLExp) + processAnnOrderByElement sourcePrefix fieldAlias annObCol = do + let ordByAlias = mkAnnOrderByAlias sourcePrefix fieldAlias similarArrayFields annObCol + (ordByAlias, ) <$> case annObCol of + AOCColumn pgColInfo -> pure $ + S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ toIden $ pgiColumn pgColInfo + + AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do + let RelInfo relName _ colMapping relTable _ = relInfo + relSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName + fieldName = mkOrderByFieldName relName + (relOrderByAlias, relOrdByExp) <- + processAnnOrderByElement relSourcePrefix fieldName rest + let selectSource = SelectSource relSourcePrefix + (S.FISimple relTable Nothing) Nothing + (toSQLBoolExp (S.QualTable relTable) relFilter) + Nothing Nothing Nothing + relSource = ObjectRelationSource relName colMapping selectSource + pure ( relSource + , HM.singleton relOrderByAlias relOrdByExp + , S.mkQIdenExp relSourcePrefix relOrderByAlias + ) + + AOCArrayAggregation relInfo relFilter aggOrderBy -> withWriteArrayRelation $ do + let RelInfo relName _ colMapping relTable _ = relInfo + fieldName = mkOrderByFieldName relName + relSourcePrefix = mkArrayRelationSourcePrefix sourcePrefix fieldAlias + similarArrayFields fieldName + relAlias = mkArrayRelationAlias fieldAlias similarArrayFields fieldName + (topExtractor, fields) = mkAggregateOrderByExtractorAndFields aggOrderBy + selectSource = SelectSource relSourcePrefix + (S.FISimple relTable Nothing) Nothing + (toSQLBoolExp (S.QualTable relTable) relFilter) + Nothing Nothing Nothing + relSource = ArrayRelationSource relAlias colMapping selectSource + pure ( relSource + , topExtractor + , HM.fromList $ aggregateFieldsToExtractorExps relSourcePrefix fields + , S.mkQIdenExp relSourcePrefix (mkAggregateOrderByAlias aggOrderBy) + ) + + toOrderByExp :: OrderByItemExp -> S.OrderByItem + toOrderByExp orderByItemExp = + let OrderByItemG obTyM expAlias obNullsM = fst . snd <$> orderByItemExp + in S.OrderByItem (S.SEIden $ toIden expAlias) + (unOrderType <$> obTyM) (unNullsOrder <$> obNullsM) + + mkCursorExp :: [OrderByItemExp] -> S.SQLExp + mkCursorExp orderByItemExps = + S.applyJsonBuildObj $ flip concatMap orderByItemExps $ + \orderByItemExp -> + let OrderByItemG _ (annObCol, (_, valExp)) _ = orderByItemExp + in annObColToJSONField valExp annObCol + where + annObColToJSONField valExp = \case + AOCColumn pgCol -> [S.SELit $ getPGColTxt $ pgiColumn pgCol, valExp] + AOCObjectRelation relInfo _ obCol -> + [ S.SELit $ relNameToTxt $ riName relInfo + , S.applyJsonBuildObj $ annObColToJSONField valExp obCol + ] + AOCArrayAggregation relInfo _ aggOrderBy -> + [ S.SELit $ relNameToTxt (riName relInfo) <> "_aggregate" + , S.applyJsonBuildObj $ + case aggOrderBy of + AAOCount -> [S.SELit "count", valExp] + AAOOp opText colInfo -> + [ S.SELit opText + , S.applyJsonBuildObj [S.SELit $ getPGColTxt $ pgiColumn colInfo, valExp] + ] + ] + +aggregateFieldsToExtractorExps + :: Iden -> AggregateFields -> [(S.Alias, S.SQLExp)] +aggregateFieldsToExtractorExps sourcePrefix aggregateFields = + flip concatMap aggregateFields $ \(_, field) -> + case field of + AFCount cty -> case cty of + S.CTStar -> [] + S.CTSimple cols -> colsToExps cols + S.CTDistinct cols -> colsToExps cols + AFOp aggOp -> aggOpToExps aggOp + AFExp _ -> [] + where colsToExps = mapMaybe (mkColExp . PCFCol) - - aggOpToExps = mapMaybe (mkColExp . snd) . _aoFlds + aggOpToExps = mapMaybe (mkColExp . snd) . _aoFields mkColExp (PCFCol c) = - let qualCol = S.mkQIdenExp (mkBaseTableAls thisPfx) (toIden c) + let qualCol = S.mkQIdenExp (mkBaseTableAlias sourcePrefix) (toIden c) colAls = toIden c in Just (S.Alias colAls, qualCol) mkColExp _ = Nothing - finalWhere = toSQLBoolExp tableQual $ - maybe permFilter (andAnnBoolExps permFilter) whereM - fromItem = selFromToFromItem baseTablepfx selectFrom - tableQual = selFromToQual selectFrom - - mkArrRelCtx arrSels = ArrRelCtx arrSels aggOrdByRelNames - - mkOrdByItems' = mkOrdByItems thisPfx fldAls orderByM strfyNum - - distItemsM = processDistinctOnCol thisPfx <$> distM - distExprM = fst <$> distItemsM - distExtrs = maybe [] snd distItemsM - - -- process an object relationship - mkObjItem (fld, objSel) = - let relName = aarName objSel - objNodePfx = mkObjRelTableAls thisPfx $ aarName objSel - objNode = mkObjNode (Prefixes objNodePfx thisPfx) (fld, objSel) - in (relName, objNode) - - -- process an array/array-aggregate item - mkArrItem arrRelCtx (fld, arrSel) = - let ArrNodeInfo arrAls arrPfx subQReq = - mkArrNodeInfo thisPfx fldAls arrRelCtx $ ANIField (fld, arrSel) - arrNode = mkArrNode subQReq (Prefixes arrPfx thisPfx) (fld, arrSel) - in (arrAls, arrNode) - - -- process a computed field, which returns a table - mkComputedFieldTable (fld, jsonAggSelect, sel) = - let prefixes = Prefixes (mkComputedFieldTableAls thisPfx fld) thisPfx - baseNode = annSelToBaseNode False prefixes fld Nothing sel - in (fld, CFTableNode jsonAggSelect baseNode) - - getAnnObj (f, annFld) = case annFld of - FObj ob -> Just (f, ob) - _ -> Nothing - - getAnnArr (f, annFld) = case annFld of - FArr ar -> Just (f, ar) - _ -> Nothing - - getComputedFieldTable (f, annFld) = case annFld of - FComputedField (CFSTable jas sel) -> Just (f, jas, sel) - _ -> Nothing - -annSelToBaseNode - :: Bool -> Prefixes -> FieldName -> Maybe (NonEmpty PGCol) -> AnnSimpleSel -> BaseNode -annSelToBaseNode subQueryReq pfxs fldAls pkeyCols annSel = - mkBaseNode subQueryReq pfxs fldAls (TAFNodes selFlds) tabFrm tabPerm tabArgs pkeyCols strfyNum - where - AnnSelG selFlds tabFrm tabPerm tabArgs strfyNum = annSel +processAnnFields + :: forall m. ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => Iden + -> FieldName + -> SimilarArrayFields + -> AnnFields + -> m (S.Alias, S.SQLExp) +processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do + fieldExps <- forM annFields $ \(fieldName, field) -> + (fieldName,) <$> + case field of + AFExpression t -> pure $ S.SELit t + + AFColumn c -> toSQLCol c + + AFObjectRelation objSel -> withWriteObjectRelation $ do + let AnnRelationSelectG relName relMapping annSel = objSel + objRelSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName + (selectSource, extractors) <- processAnnSimpleSelect (mkSourcePrefixes objRelSourcePrefix) + fieldName PLSQNotRequired annSel + let objRelSource = ObjectRelationSource relName relMapping selectSource + pure ( objRelSource + , extractors + , S.mkQIdenExp objRelSourcePrefix fieldName + ) -mkObjNode :: Prefixes -> (FieldName, ObjSel) -> ObjNode -mkObjNode pfxs (fldName, AnnRelG _ rMapn rAnnSel) = - ObjNode rMapn $ annSelToBaseNode False pfxs fldName Nothing rAnnSel + AFArrayRelation arrSel -> do + let arrRelSourcePrefix = mkArrayRelationSourcePrefix sourcePrefix fieldAlias similarArrFields fieldName + arrRelAlias = mkArrayRelationAlias fieldAlias similarArrFields fieldName + processArrayRelation (mkSourcePrefixes arrRelSourcePrefix) fieldName arrRelAlias arrSel + pure $ S.mkQIdenExp arrRelSourcePrefix fieldName + + AFComputedField (CFSScalar scalar) -> fromScalarComputedField scalar + + AFComputedField (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do + let computedFieldSourcePrefix = + mkComputedFieldTableAlias sourcePrefix fieldName + (selectSource, nodeExtractors) <- + processAnnSimpleSelect (mkSourcePrefixes computedFieldSourcePrefix) + fieldName PLSQNotRequired sel + let computedFieldTableSetSource = + ComputedFieldTableSetSource fieldName selectTy selectSource + pure ( computedFieldTableSetSource + , nodeExtractors + , S.mkQIdenExp computedFieldSourcePrefix fieldName + ) -mkArrNode :: Bool -> Prefixes -> (FieldName, ArrSel) -> ArrNode -mkArrNode subQueryReq pfxs (fldName, annArrSel) = case annArrSel of - ASSimple annArrRel -> - let bn = annSelToBaseNode subQueryReq pfxs fldName Nothing $ aarAnnSel annArrRel - permLimit = getPermLimit $ aarAnnSel annArrRel - extr = asJsonAggExtr JASMultipleRows (S.toAlias fldName) subQueryReq permLimit $ - _bnOrderBy bn - in ArrNode [extr] (aarMapping annArrRel) bn + pure $ + -- posttgres ignores anything beyond 63 chars for an iden + -- in this case, we'll need to use json_build_object function + -- json_build_object is slower than row_to_json hence it is only + -- used when needed + if any ( (> 63) . T.length . getFieldNameTxt . fst ) fieldExps then + withJsonBuildObj fieldAlias $ concatMap toJsonBuildObjectExps fieldExps + else withRowToJSON fieldAlias $ map toRowToJsonExtr fieldExps + where + mkSourcePrefixes newPrefix = SourcePrefixes newPrefix sourcePrefix + toJsonBuildObjectExps (fieldName, fieldExp) = + [S.SELit $ getFieldNameTxt fieldName, fieldExp] + + toRowToJsonExtr (fieldName, fieldExp) = + S.Extractor fieldExp $ Just $ S.toAlias fieldName + + toSQLCol :: AnnColumnField -> m S.SQLExp + toSQLCol (AnnColumnField col asText colOpM) = do + strfyNum <- ask + pure $ toJSONableExp strfyNum (pgiType col) asText $ withColumnOp colOpM $ + S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ pgiColumn col + + fromScalarComputedField :: ComputedFieldScalarSelect S.SQLExp -> m S.SQLExp + fromScalarComputedField computedFieldScalar = do + strfyNum <- ask + pure $ toJSONableExp strfyNum (PGColumnScalar ty) False $ withColumnOp colOpM $ + S.SEFunction $ S.FunctionExp fn (fromTableRowArgs sourcePrefix args) Nothing + where + ComputedFieldScalarSelect fn args ty colOpM = computedFieldScalar - ASAgg annAggSel -> aggSelToArrNode pfxs fldName annAggSel - ASConnection connectionSel -> connectionSelToArrNode pfxs fldName connectionSel + withColumnOp :: Maybe ColumnOp -> S.SQLExp -> S.SQLExp + withColumnOp colOpM sqlExp = case colOpM of + Nothing -> sqlExp + Just (ColumnOp opText cExp) -> S.mkSQLOpExp opText sqlExp cExp injectJoinCond :: S.BoolExp -- ^ Join condition -> S.BoolExp -- ^ Where condition @@ -715,28 +807,33 @@ mkJoinCond baseTablepfx colMapn = foldl' (S.BEBin S.AndOp) (S.BELit True) $ flip map (HM.toList colMapn) $ \(lCol, rCol) -> S.BECompare S.SEQ (S.mkQIdenExp baseTablepfx lCol) (S.mkSIdenExp rCol) -baseNodeToSel :: S.BoolExp -> BaseNode -> S.Select -baseNodeToSel joinCond baseNode = +generateSQLSelect + :: S.BoolExp -- ^ Pre join condition + -> SelectSource + -> SelectNode + -> S.Select +generateSQLSelect joinCondition selectSource selectNode = S.mkSelect - { S.selExtr = [S.Extractor e $ Just a | (a, e) <- HM.toList extrs] - , S.selFrom = Just $ S.FromExp [joinedFrom] - , S.selOrderBy = ordByM - , S.selLimit = S.LimitExp . S.intToSQLExp <$> limitM - , S.selOffset = S.OffsetExp <$> offsetM - , S.selDistinct = dExp + { S.selExtr = [S.Extractor e $ Just a | (a, e) <- HM.toList extractors] + , S.selFrom = Just $ S.FromExp [joinedFrom] + , S.selOrderBy = maybeOrderby + , S.selLimit = S.LimitExp . S.intToSQLExp <$> maybeLimit + , S.selOffset = S.OffsetExp <$> maybeOffset + , S.selDistinct = maybeDistinct } where - BaseNode pfx dExp fromItem whr ordByM limitM - offsetM extrs objRels arrRels computedFields - = baseNode - -- this is the table which is aliased as "pfx.base" - baseSel = S.mkSelect + SelectSource sourcePrefix fromItem maybeDistinct whereExp + maybeOrderby maybeLimit maybeOffset = selectSource + SelectNode extractors joinTree = selectNode + JoinTree objectRelations arrayRelations arrayConnections computedFields = joinTree + -- this is the table which is aliased as "sourcePrefix.base" + baseSelect = S.mkSelect { S.selExtr = [S.Extractor (S.SEStar Nothing) Nothing] , S.selFrom = Just $ S.FromExp [fromItem] - , S.selWhere = Just $ injectJoinCond joinCond whr + , S.selWhere = Just $ injectJoinCond joinCondition whereExp } - baseSelAls = S.Alias $ mkBaseTableAls pfx - baseFromItem = S.FISelect (S.Lateral False) baseSel baseSelAls + baseSelectAlias = S.Alias $ mkBaseTableAlias sourcePrefix + baseFromItem = S.FISelect (S.Lateral False) baseSelect baseSelectAlias -- function to create a joined from item from two from items leftOuterJoin current new = @@ -746,222 +843,403 @@ baseNodeToSel joinCond baseNode = -- this is the from eexp for the final select joinedFrom :: S.FromItem joinedFrom = foldl' leftOuterJoin baseFromItem $ - map objNodeToFromItem (HM.elems objRels) <> - map arrNodeToFromItem (HM.elems arrRels) <> - map computedFieldNodeToFromItem (HM.toList computedFields) - - objNodeToFromItem :: ObjNode -> S.FromItem - objNodeToFromItem (ObjNode relMapn relBaseNode) = - let als = S.Alias $ _bnPrefix relBaseNode - sel = baseNodeToSel (mkJoinCond baseSelAls relMapn) relBaseNode - in S.mkLateralFromItem sel als - - arrNodeToFromItem :: ArrNode -> S.FromItem - arrNodeToFromItem (ArrNode es colMapn bn) = - let sel = arrNodeToSelect bn es (mkJoinCond baseSelAls colMapn) - als = S.Alias $ _bnPrefix bn - in S.mkLateralFromItem sel als - - computedFieldNodeToFromItem :: (FieldName, CFTableNode) -> S.FromItem - computedFieldNodeToFromItem (fld, CFTableNode jsonAggSelect bn) = - let internalSel = baseNodeToSel (S.BELit True) bn - als = S.Alias $ _bnPrefix bn - extr = asJsonAggExtr jsonAggSelect (S.toAlias fld) False Nothing $ - _bnOrderBy bn - internalSelFrom = S.mkSelFromItem internalSel als - sel = S.mkSelect - { S.selExtr = pure extr - , S.selFrom = Just $ S.FromExp [internalSelFrom] + map objectRelationToFromItem (HM.toList objectRelations) <> + map arrayRelationToFromItem (HM.toList arrayRelations) <> + map arrayConnectionToFromItem (HM.toList arrayConnections) <> + map computedFieldToFromItem (HM.toList computedFields) + + + objectRelationToFromItem + :: (ObjectRelationSource, SelectNode) -> S.FromItem + objectRelationToFromItem (objectRelationSource, node) = + let ObjectRelationSource _ colMapping source = objectRelationSource + alias = S.Alias $ _ssPrefix source + select = generateSQLSelect (mkJoinCond baseSelectAlias colMapping) source node + in S.mkLateralFromItem select alias + + arrayRelationToFromItem + :: (ArrayRelationSource, ArraySelectNode) -> S.FromItem + arrayRelationToFromItem (arrayRelationSource, arraySelectNode) = + let ArrayRelationSource _ colMapping source = arrayRelationSource + alias = S.Alias $ _ssPrefix source + select = generateSQLSelectFromArrayNode source arraySelectNode $ + mkJoinCond baseSelectAlias colMapping + in S.mkLateralFromItem select alias + + arrayConnectionToFromItem + :: (ArrayConnectionSource, ArraySelectNode) -> S.FromItem + arrayConnectionToFromItem (arrayConnectionSource, arraySelectNode) = + let selectWith = connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode + alias = S.Alias $ _ssPrefix $ _acsSource arrayConnectionSource + in S.FISelectWith (S.Lateral True) selectWith alias + + computedFieldToFromItem + :: (ComputedFieldTableSetSource, SelectNode) -> S.FromItem + computedFieldToFromItem (computedFieldTableSource, node) = + let ComputedFieldTableSetSource fieldName selectTy source = computedFieldTableSource + internalSelect = generateSQLSelect (S.BELit True) source node + extractor = asJsonAggExtr selectTy (S.toAlias fieldName) PLSQNotRequired $ + _ssOrderBy source + alias = S.Alias $ _ssPrefix source + select = S.mkSelect + { S.selExtr = [extractor] + , S.selFrom = Just $ S.FromExp [S.mkSelFromItem internalSelect alias] } - in S.mkLateralFromItem sel als - -data TableSource - = TableSource - { _tsFrom :: !SelectFrom - , _tsPermission :: !TablePerm - , _tsArgs :: !TableArgs - } deriving (Show, Eq, Generic) -instance Hashable TableSource - - --- data TableSource --- = TableSource --- { _tsFrom :: !S.FromItem --- , _tsAlias :: !S.Alias --- , _tsWhere :: !S.BoolExp --- , _tsLimit :: !(Maybe Int) --- , _tsOffset :: !(Maybe S.SQLExp) --- -- , _tsArgs :: !TableArgs --- } --- deriving (Show, Eq, Generic) - - -data BuildState - = BuildState - { _bsAliasGenerator :: !Int - , _bsJoinTree :: !JoinTree - } - -type Build = State BuildState - --- | this is need in those cases where you need a single row when the table --- returns multiple -forceAggregation :: S.SQLExp -> S.SQLExp -forceAggregation e = - S.SEFnApp "coalesce" [ e, S.SEUnsafe "bool_or('true')::text"] Nothing - --- | Returns the SQL expression that will construct the response --- for given field -buildConnectionFieldExpression - :: S.Alias - -- ^ The table alias - -> ConnectionField S.SQLExp - -> Build S.SQLExp -buildConnectionFieldExpression tableAlias = \case - ConnectionTypename typename -> pure $ forceAggregation $ S.SELit typename - ConnectionPageInfo fields -> - pure $ S.SELit "should be pageinfo" - ConnectionEdges fields -> do - fieldPairs <- forM fields $ \(fieldAlias, field) -> do - fieldExpression <- buildEdgeFieldExpression field - pure [S.SELit $ getFieldNameTxt fieldAlias, fieldExpression] - pure $ S.applyJsonBuildObj $ concat fieldPairs + in S.mkLateralFromItem select alias + +generateSQLSelectFromArrayNode + :: SelectSource + -> ArraySelectNode + -> S.BoolExp + -> S.Select +generateSQLSelectFromArrayNode selectSource arraySelectNode joinCondition = + S.mkSelect + { S.selExtr = topExtractors + , S.selFrom = Just $ S.FromExp [selectFrom] + } where - buildEdgeFieldExpression = \case - EdgeTypename typename -> pure $ S.SELit typename - EdgeCursor -> pure $ S.SELit "will return a cursor" - EdgeNode fields -> undefined - -data Selector - = Selector !S.SQLExp !S.SQLExp - deriving (Show, Eq) - --- | Returns the SQL expression that will construct the response --- for given field -buildTableFieldExpression - :: S.Alias - -- ^ The table alias - -> AnnFldG S.SQLExp - -> Build S.SQLExp -buildTableFieldExpression tableAlias = \case - FCol field -> undefined - FObj field -> undefined - FArr field -> undefined - FComputedField field -> undefined - FExp typename -> pure $ S.SELit typename - -processFields - :: ConnectionFields S.SQLExp - -> Maybe (NE.NonEmpty (AnnOrderByItemG S.SQLExp)) - -> Build () -processFields = undefined - -mkSelectNode - :: ConnectionSelect S.SQLExp -> Build (TableSource, SelectNode) -mkSelectNode = undefined - -data SelectNode - = SelectNode - { _jnColumns :: !(HM.HashMap S.Alias S.SQLExp) - , _jnFrom :: !S.FromItem - , _jnAlias :: !S.Alias - , _jnJoinTree :: !JoinTree - , _jnWhere :: !S.BoolExp - , _jnOrderBy :: !(Maybe S.OrderByExp) - , _jnLimit :: !(Maybe Int) - , _jnOffset :: !(Maybe S.SQLExp) - } - deriving (Show, Eq) - -newtype JoinTree - = JoinTree { unJoinTree :: HM.HashMap TableSource SelectNode } - deriving (Show, Eq, Semigroup, Monoid) - -mkAggSelect :: AnnAggSel -> S.Select -mkAggSelect annAggSel = - prefixNumToAliases $ arrNodeToSelect bn extr $ S.BELit True + ArraySelectNode topExtractors selectNode = arraySelectNode + selectFrom = S.mkSelFromItem + (generateSQLSelect joinCondition selectSource selectNode) $ + S.Alias $ _ssPrefix selectSource + +mkAggregateSelect :: AnnAggregateSelect -> S.Select +mkAggregateSelect annAggSel = + let ((selectSource, nodeExtractors, topExtractor), joinTree) = + runWriter $ flip runReaderT strfyNum $ + processAnnAggregateSelect sourcePrefixes rootFieldName annAggSel + selectNode = SelectNode nodeExtractors joinTree + arrayNode = ArraySelectNode [topExtractor] selectNode + in prefixNumToAliases $ + generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True where - aggSel = AnnRelG rootRelName HM.empty annAggSel - rootIden = Iden "root" - rootPrefix = Prefixes rootIden rootIden - ArrNode extr _ bn = - aggSelToArrNode rootPrefix (FieldName "root") aggSel + strfyNum = _asnStrfyNum annAggSel + rootFieldName = FieldName "root" + rootIden = toIden rootFieldName + sourcePrefixes = SourcePrefixes rootIden rootIden mkSQLSelect :: JsonAggSelect -> AnnSimpleSel -> S.Select mkSQLSelect jsonAggSelect annSel = - prefixNumToAliases $ arrNodeToSelect baseNode extrs $ S.BELit True + let permLimitSubQuery = PLSQNotRequired + ((selectSource, nodeExtractors), joinTree) = + runWriter $ flip runReaderT strfyNum $ + processAnnSimpleSelect sourcePrefixes rootFldName permLimitSubQuery annSel + selectNode = SelectNode nodeExtractors joinTree + topExtractor = asJsonAggExtr jsonAggSelect rootFldAls permLimitSubQuery + $ _ssOrderBy selectSource + arrayNode = ArraySelectNode [topExtractor] selectNode + in prefixNumToAliases $ + generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True where - permLimit = getPermLimit annSel - extrs = pure $ asJsonAggExtr jsonAggSelect rootFldAls False permLimit - $ _bnOrderBy baseNode + strfyNum = _asnStrfyNum annSel rootFldIden = toIden rootFldName - rootPrefix = Prefixes rootFldIden rootFldIden - baseNode = annSelToBaseNode False rootPrefix rootFldName Nothing annSel + sourcePrefixes = SourcePrefixes rootFldIden rootFldIden rootFldName = FieldName "root" rootFldAls = S.Alias $ toIden rootFldName -mkConnectionSelectNodeAndExtr - :: Prefixes +mkConnectionSelect :: ConnectionSelect S.SQLExp -> S.SelectWithG S.Select +mkConnectionSelect connectionSelect = + let ((connectionSource, topExtractor, nodeExtractors), joinTree) = + runWriter $ flip runReaderT strfyNum $ + processConnectionSelect sourcePrefixes rootFieldName + (S.Alias rootIden) mempty connectionSelect + selectNode = ArraySelectNode [topExtractor] $ + SelectNode nodeExtractors joinTree + in prefixNumToAliasesSelectWith $ + connectionToSelectWith (S.Alias rootIden) connectionSource selectNode + where + strfyNum = _asnStrfyNum $ _csSelect connectionSelect + rootFieldName = FieldName "root" + rootIden = toIden rootFieldName + sourcePrefixes = SourcePrefixes rootIden rootIden + +-- | First element extractor expression from given record set +-- For example:- To get first "id" column from given row set, +-- the function generates the SQL expression AS `(array_agg("id"))[1]` +mkFirstElementExp :: S.SQLExp -> S.SQLExp +mkFirstElementExp expIden = + -- For Example + S.SEArrayIndex (S.SEFnApp "array_agg" [expIden] Nothing) (S.intToSQLExp 1) + +-- | Last element extractor expression from given record set. +-- For example:- To get first "id" column from given row set, +-- the function generates the SQL expression AS `(array_agg("id"))[array_length(array_agg("id"), 1)]` +mkLastElementExp :: S.SQLExp -> S.SQLExp +mkLastElementExp expIden = + let arrayExp = S.SEFnApp "array_agg" [expIden] Nothing + in S.SEArrayIndex arrayExp $ + S.SEFnApp "array_length" [arrayExp, S.intToSQLExp 1] Nothing + +cursorIden :: Iden +cursorIden = Iden "__cursor" + +startCursorIden :: Iden +startCursorIden = Iden "__start_cursor" + +endCursorIden :: Iden +endCursorIden = Iden "__end_cursor" + +hasPreviousPageIden :: Iden +hasPreviousPageIden = Iden "__has_previous_page" + +hasNextPageIden :: Iden +hasNextPageIden = Iden "__has_next_page" + +pageInfoSelectAliasIden :: Iden +pageInfoSelectAliasIden = Iden "__page_info" + +cursorsSelectAliasIden :: Iden +cursorsSelectAliasIden = Iden "__cursors_select" + +processConnectionSelect + :: ( MonadReader Bool m + , MonadWriter JoinTree m + ) + => SourcePrefixes -> FieldName - -> Maybe (NonEmpty PGCol) + -> S.Alias + -> HM.HashMap PGCol PGCol -> ConnectionSelect S.SQLExp - -> (BaseNode, S.Extractor) -mkConnectionSelectNodeAndExtr pfxs fieldAlias primaryKeyColumns connectionSelect = - (baseNode, extractor) + -> m ( ArrayConnectionSource + , S.Extractor + , HM.HashMap S.Alias S.SQLExp + ) +processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connectionSelect = do + (selectSource, orderByAndDistinctExtrs, maybeOrderByCursor) <- + processSelectParams sourcePrefixes fieldAlias similarArrayFields selectFrom + permLimitSubQuery tablePermissions tableArgs + + let mkCursorExtractor = (S.Alias cursorIden,) . (`S.SETyAnn` S.textTypeAnn) + cursorExtractors = case maybeOrderByCursor of + Just orderByCursor -> [mkCursorExtractor orderByCursor] + Nothing -> + -- Extract primary key columns from base select along with cursor expression. + -- Those columns are required to perform connection split via a WHERE clause. + mkCursorExtractor primaryKeyColumnsCursor : primaryKeyColumnExtractors + orderByExp = _ssOrderBy selectSource + (topExtractorExp, exps) <- flip runStateT [] $ processFields orderByExp + let topExtractor = S.Extractor topExtractorExp $ Just $ S.Alias fieldIden + allExtractors = HM.fromList $ cursorExtractors <> exps <> orderByAndDistinctExtrs + arrayConnectionSource = ArrayConnectionSource relAlias + colMapping splitBoolExp slice selectSource + pure ( arrayConnectionSource + , topExtractor + , allExtractors + ) where - AnnSelG fields selFrom perm args strfyNum = connectionSelect + ConnectionSelect primaryKeyColumns split slice select = connectionSelect + AnnSelectG fields selectFrom tablePermissions tableArgs _ = select fieldIden = toIden fieldAlias + thisPrefix = _pfThis sourcePrefixes + permLimitSubQuery = PLSQNotRequired + + primaryKeyColumnsCursor = + S.applyJsonBuildObj $ flip concatMap (toList primaryKeyColumns) $ + \pgColumnInfo -> + [ S.SELit $ getPGColTxt $ pgiColumn pgColumnInfo + , toJSONableExp False (pgiType pgColumnInfo) False $ + S.mkQIdenExp (mkBaseTableAlias thisPrefix) $ pgiColumn pgColumnInfo + ] + + primaryKeyColumnExtractors = + flip map (toList primaryKeyColumns) $ + \pgColumnInfo -> + let pgColumn = pgiColumn pgColumnInfo + in ( S.Alias $ mkBaseTableColumnAlias thisPrefix pgColumn + , S.mkQIdenExp (mkBaseTableAlias thisPrefix) pgColumn + ) + + splitBoolExp = flip fmap split $ \splits -> + foldr (S.BEBin S.OrOp) (S.BELit False) $ + flip map (toList splits) $ \s -> + let ConnectionSplit kind v (OrderByItemG obTyM obCol _) = s + obAlias = mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields obCol + obTy = maybe S.OTAsc unOrderType obTyM + compareOp = case (kind, obTy) of + (CSKAfter, S.OTAsc) -> S.SGT + (CSKAfter, S.OTDesc) -> S.SLT + (CSKBefore, S.OTAsc) -> S.SLT + (CSKBefore, S.OTDesc) -> S.SGT + in S.BECompare compareOp (S.SEIden $ toIden obAlias) v + + similarArrayFields = HM.unions $ + flip map (map snd fields) $ \case + ConnectionTypename{} -> mempty + ConnectionPageInfo{} -> mempty + ConnectionEdges edges -> HM.unions $ + flip map (map snd edges) $ \case + EdgeTypename{} -> mempty + EdgeCursor{} -> mempty + EdgeNode annFields -> + mkSimilarArrayFields annFields $ _saOrderBy tableArgs + + encodeBase64 t = S.SEFnApp "encode" + [S.SETyAnn t $ S.TypeAnn "bytea", S.SELit "base64"] + Nothing + mkSimpleJsonAgg rowExp ob = let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing - baseNode = foldr mergeBaseNodes (mkEmptyBaseNode fieldIden selFrom) nodeList - encodeBase64 t = S.SEFnApp "encode" [S.SETyAnn t $ S.TypeAnn "bytea", S.SELit "base64"] Nothing - - extractor = S.Extractor extractorExp $ Just $ S.Alias fieldIden - (extractorExp, nodeList) = runWriter $ - fmap (S.applyJsonBuildObj . concat) $ forM fields $ + processFields + :: ( MonadReader Bool m + , MonadWriter JoinTree m + , MonadState [(S.Alias, S.SQLExp)] m + ) + => Maybe S.OrderByExp -> m S.SQLExp + processFields orderByExp = + fmap (S.applyJsonBuildObj . concat) $ + forM fields $ \(FieldName fieldText, field) -> (S.SELit fieldText:) . pure <$> case field of - ConnectionTypename t -> pure $ S.SELit t - ConnectionPageInfo f -> pure $ processPageInfoFields f + ConnectionTypename t -> pure $ S.SELit t + ConnectionPageInfo pageInfoFields -> pure $ processPageInfoFields pageInfoFields ConnectionEdges edges -> - -- TODO:- Add order by here - fmap (flip mkSimpleJsonAgg Nothing . S.applyJsonBuildObj . concat) $ forM edges $ + fmap (flip mkSimpleJsonAgg orderByExp . S.applyJsonBuildObj . concat) $ forM edges $ \(FieldName edgeText, edge) -> (S.SELit edgeText:) . pure <$> case edge of EdgeTypename t -> pure $ S.SELit t - EdgeCursor -> pure $ encodeBase64 $ S.SEIden (toIden cursorAlias) + EdgeCursor -> pure $ encodeBase64 $ S.SEIden (toIden cursorIden) EdgeNode annFields -> do - let rootFieldName = FieldName $ "root." <> fieldText <> "." <> edgeText - rootFieldIden = toIden rootFieldName - edgeBaseNode = annSelToBaseNode False pfxs rootFieldName primaryKeyColumns $ - AnnSelG annFields selFrom perm args strfyNum - tell [edgeBaseNode] - pure $ S.SEIden rootFieldIden + let edgeFieldName = FieldName $ + getFieldNameTxt fieldAlias <> "." <> fieldText <> "." <> edgeText + edgeFieldIden = toIden edgeFieldName + annFieldsExtrExp <- processAnnFields thisPrefix edgeFieldName similarArrayFields annFields + modify' (<> [annFieldsExtrExp]) + pure $ S.SEIden edgeFieldIden processPageInfoFields infoFields = S.applyJsonBuildObj $ flip concatMap infoFields $ - \(FieldName fieldText, field) -> S.SELit fieldText: case field of - PageInfoTypename t -> pure $ S.SELit t - PageInfoHasNextPage -> pure $ S.SEBool $ S.BELit True - PageInfoHasPreviousPage -> pure $ S.SEBool $ S.BELit False - PageInfoStartCursor -> pure $ S.SELit "start cursor" - PageInfoEndCursor -> pure $ S.SELit "end cursor" - -connectionSelToArrNode - :: Prefixes -> FieldName -> ArrRelConnection S.SQLExp -> ArrNode -connectionSelToArrNode pfxs als arrRelConnSel = - -- TODO:- Get primary key context here. - let (baseNode, extr) = mkConnectionSelectNodeAndExtr pfxs als Nothing connSel - in ArrNode [extr] colMapping baseNode + \(FieldName fieldText, field) -> (:) (S.SELit fieldText) $ pure case field of + PageInfoTypename t -> S.SELit t + PageInfoHasNextPage -> + mkSingleFieldSelect (S.SEIden hasNextPageIden) pageInfoSelectAliasIden + PageInfoHasPreviousPage -> + mkSingleFieldSelect (S.SEIden hasPreviousPageIden) pageInfoSelectAliasIden + PageInfoStartCursor -> + encodeBase64 $ mkSingleFieldSelect (S.SEIden startCursorIden) cursorsSelectAliasIden + PageInfoEndCursor -> + encodeBase64 $ mkSingleFieldSelect (S.SEIden endCursorIden) cursorsSelectAliasIden + where + mkSingleFieldSelect field fromIden = S.SESelect + S.mkSelect { S.selExtr = [S.Extractor field Nothing] + , S.selFrom = Just $ S.FromExp [S.FIIden fromIden] + } + +connectionToSelectWith + :: S.Alias + -> ArrayConnectionSource + -> ArraySelectNode + -> S.SelectWithG S.Select +connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode = + let extractionSelect = S.mkSelect + { S.selExtr = topExtractors + , S.selFrom = Just $ S.FromExp [S.FIIden finalSelectIden] + } + in S.SelectWith fromBaseSelections extractionSelect where - AnnRelG _ colMapping connSel = arrRelConnSel - -mkConnectionSelect :: Maybe (NonEmpty PGCol) -> ConnectionSelect S.SQLExp -> S.Select -mkConnectionSelect primaryKeyColumns connectionSelect = - let rootFieldName = FieldName "root" - rootIden = toIden rootFieldName - prefixes = Prefixes rootIden rootIden - (baseNode, extractor) = mkConnectionSelectNodeAndExtr prefixes rootFieldName primaryKeyColumns connectionSelect - in prefixNumToAliases $ arrNodeToSelect baseNode [extractor] $ S.BELit True + ArrayConnectionSource _ columnMapping maybeSplit maybeSlice selectSource = + arrayConnectionSource + ArraySelectNode topExtractors selectNode = arraySelectNode + baseSelectIden = Iden "__base_select" + splitSelectIden = Iden "__split_select" + sliceSelectIden = Iden "__slice_select" + finalSelectIden = Iden "__final_select" + + rowNumberIden = Iden "__row_number" + rowNumberExp = S.SEUnsafe "(row_number() over (partition by 1))" + startRowNumberIden = Iden "__start_row_number" + endRowNumberIden = Iden "__end_row_number" + + startCursorExp = mkFirstElementExp $ S.SEIden cursorIden + endCursorExp = mkLastElementExp $ S.SEIden cursorIden + + startRowNumberExp = mkFirstElementExp $ S.SEIden rowNumberIden + endRowNumberExp = mkLastElementExp $ S.SEIden rowNumberIden + + fromBaseSelections = + let joinCond = mkJoinCond baseSelectAlias columnMapping + baseSelectFrom = S.mkSelFromItem + (generateSQLSelect joinCond selectSource selectNode) + $ S.Alias $ _ssPrefix selectSource + select = + S.mkSelect { S.selExtr = [ S.selectStar + , S.Extractor rowNumberExp $ Just $ S.Alias rowNumberIden + ] + , S.selFrom = Just $ S.FromExp [baseSelectFrom] + } + in (S.Alias baseSelectIden, select):fromSplitSelection + + mkStarSelect fromIden = + S.mkSelect { S.selExtr = [S.selectStar] + , S.selFrom = Just $ S.FromExp [S.FIIden fromIden] + } + + fromSplitSelection = case maybeSplit of + Nothing -> fromSliceSelection baseSelectIden + Just splitBool -> + let select = + (mkStarSelect baseSelectIden){S.selWhere = Just $ S.WhereFrag splitBool} + in (S.Alias splitSelectIden, select):fromSliceSelection splitSelectIden + + fromSliceSelection prevSelect = case maybeSlice of + Nothing -> fromFinalSelect prevSelect + Just slice -> + let select = case slice of + SliceFirst limit -> + (mkStarSelect prevSelect) + {S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit} + SliceLast limit -> + let mkRowNumberOrderBy obType = + let orderByItem = + S.OrderByItem (S.SEIden rowNumberIden) (Just obType) Nothing + in S.OrderByExp $ orderByItem NE.:| [] + + sliceLastSelect = (mkStarSelect prevSelect) + { S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit + , S.selOrderBy = Just $ mkRowNumberOrderBy S.OTDesc + } + sliceLastSelectFrom = + S.mkSelFromItem sliceLastSelect $ S.Alias sliceSelectIden + in S.mkSelect { S.selExtr = [S.selectStar] + , S.selFrom = Just $ S.FromExp [sliceLastSelectFrom] + , S.selOrderBy = Just $ mkRowNumberOrderBy S.OTAsc + } + in (S.Alias sliceSelectIden, select):fromFinalSelect sliceSelectIden + + fromFinalSelect prevSelect = + let select = mkStarSelect prevSelect + in (S.Alias finalSelectIden, select):fromCursorSelection + + fromCursorSelection = + let extrs = [ S.Extractor startCursorExp $ Just $ S.Alias startCursorIden + , S.Extractor endCursorExp $ Just $ S.Alias endCursorIden + , S.Extractor startRowNumberExp $ Just $ S.Alias startRowNumberIden + , S.Extractor endRowNumberExp $ Just $ S.Alias endRowNumberIden + ] + select = + S.mkSelect { S.selExtr = extrs + , S.selFrom = Just $ S.FromExp [S.FIIden finalSelectIden] + } + in (S.Alias cursorsSelectAliasIden, select):fromPageInfoSelection + + fromPageInfoSelection = + let hasPrevPage = S.SEBool $ + S.mkExists (S.FIIden baseSelectIden) $ + S.BECompare S.SLT (S.SEIden rowNumberIden) $ + S.SESelect $ S.mkSelect { S.selFrom = Just $ S.FromExp [S.FIIden cursorsSelectAliasIden] + , S.selExtr = [S.Extractor (S.SEIden startRowNumberIden) Nothing] + } + hasNextPage = S.SEBool $ + S.mkExists (S.FIIden baseSelectIden) $ + S.BECompare S.SGT (S.SEIden rowNumberIden) $ + S.SESelect $ S.mkSelect { S.selFrom = Just $ S.FromExp [S.FIIden cursorsSelectAliasIden] + , S.selExtr = [S.Extractor (S.SEIden endRowNumberIden) Nothing] + } + + select = + S.mkSelect { S.selExtr = [ S.Extractor hasPrevPage $ Just $ S.Alias hasPreviousPageIden + , S.Extractor hasNextPage $ Just $ S.Alias hasNextPageIden + ] + } + in pure (S.Alias pageInfoSelectAliasIden, select) diff --git a/server/src-lib/Hasura/RQL/DML/Select/Types.hs b/server/src-lib/Hasura/RQL/DML/Select/Types.hs index 759e1d6b8b201..b78b6ac358639 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Types.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Types.hs @@ -21,7 +21,8 @@ type SelectQExt = SelectG ExtCol BoolExp Int data JsonAggSelect = JASMultipleRows | JASSingleObject - deriving (Show, Eq) + deriving (Show, Eq, Generic) +instance Hashable JsonAggSelect -- Columns in RQL data ExtCol @@ -50,163 +51,168 @@ instance FromJSON ExtCol where , "object (relationship)" ] -data AnnAggOrdBy +data AnnAggregateOrderBy = AAOCount - | AAOOp !T.Text !PGCol + | AAOOp !T.Text !PGColumnInfo deriving (Show, Eq, Generic) -instance Hashable AnnAggOrdBy +instance Hashable AnnAggregateOrderBy -data AnnObColG v - = AOCPG !PGCol - | AOCObj !RelInfo !(AnnBoolExp v) !(AnnObColG v) - | AOCAgg !RelInfo !(AnnBoolExp v) !AnnAggOrdBy - deriving (Show, Eq, Generic) -instance (Hashable v) => Hashable (AnnObColG v) +data AnnOrderByElementG v + = AOCColumn !PGColumnInfo + | AOCObjectRelation !RelInfo !v !(AnnOrderByElementG v) + | AOCArrayAggregation !RelInfo !v !AnnAggregateOrderBy + deriving (Show, Eq, Generic, Functor) +instance (Hashable v) => Hashable (AnnOrderByElementG v) -traverseAnnObCol +type AnnOrderByElement v = AnnOrderByElementG (AnnBoolExp v) + +traverseAnnOrderByElement :: (Applicative f) - => (a -> f b) -> AnnObColG a -> f (AnnObColG b) -traverseAnnObCol f = \case - AOCPG pgColInfo -> pure $ AOCPG pgColInfo - AOCObj relInfo annBoolExp annObCol -> - AOCObj relInfo + => (a -> f b) -> AnnOrderByElement a -> f (AnnOrderByElement b) +traverseAnnOrderByElement f = \case + AOCColumn pgColInfo -> pure $ AOCColumn pgColInfo + AOCObjectRelation relInfo annBoolExp annObCol -> + AOCObjectRelation relInfo <$> traverseAnnBoolExp f annBoolExp - <*> traverseAnnObCol f annObCol - AOCAgg relInfo annBoolExp annAggOb -> - AOCAgg relInfo + <*> traverseAnnOrderByElement f annObCol + AOCArrayAggregation relInfo annBoolExp annAggOb -> + AOCArrayAggregation relInfo <$> traverseAnnBoolExp f annBoolExp <*> pure annAggOb -type AnnObCol = AnnObColG S.SQLExp - -type AnnOrderByItemG v = OrderByItemG (AnnObColG v) +type AnnOrderByItemG v = OrderByItemG (AnnOrderByElement v) traverseAnnOrderByItem :: (Applicative f) => (a -> f b) -> AnnOrderByItemG a -> f (AnnOrderByItemG b) traverseAnnOrderByItem f = - traverse (traverseAnnObCol f) + traverse (traverseAnnOrderByElement f) type AnnOrderByItem = AnnOrderByItemG S.SQLExp -data AnnRelG a - = AnnRelG - { aarName :: !RelName -- Relationship name - , aarMapping :: !(HashMap PGCol PGCol) -- Column of left table to join with - , aarAnnSel :: !a -- Current table. Almost ~ to SQL Select +type OrderByItemExp = + OrderByItemG (AnnOrderByElement S.SQLExp, (S.Alias, S.SQLExp)) + +data AnnRelationSelectG a + = AnnRelationSelectG + { aarRelationshipName :: !RelName -- Relationship name + , aarColumnMapping :: !(HashMap PGCol PGCol) -- Column of left table to join with + , aarAnnSelect :: !a -- Current table. Almost ~ to SQL Select } deriving (Show, Eq, Functor, Foldable, Traversable) -type ObjSelG v = AnnRelG (AnnSimpleSelG v) -type ObjSel = ObjSelG S.SQLExp +type ObjectRelationSelectG v = AnnRelationSelectG (AnnSimpleSelG v) +type ObjectRelationSelect = ObjectRelationSelectG S.SQLExp -type ArrRelG v = AnnRelG (AnnSimpleSelG v) -type ArrRelAggG v = AnnRelG (AnnAggSelG v) -type ArrRelConnection v = AnnRelG (ConnectionSelect v) -type ArrRelAgg = ArrRelAggG S.SQLExp +type ArrayRelationSelectG v = AnnRelationSelectG (AnnSimpleSelG v) +type ArrayAggregateSelectG v = AnnRelationSelectG (AnnAggregateSelectG v) +type ArrayConnectionSelect v = AnnRelationSelectG (ConnectionSelect v) +type ArrayAggregateSelect = ArrayAggregateSelectG S.SQLExp -data ComputedFieldScalarSel v - = ComputedFieldScalarSel +data ComputedFieldScalarSelect v + = ComputedFieldScalarSelect { _cfssFunction :: !QualifiedFunction , _cfssArguments :: !(FunctionArgsExpTableRow v) , _cfssType :: !PGScalarType - , _cfssColumnOp :: !(Maybe ColOp) + , _cfssColumnOp :: !(Maybe ColumnOp) } deriving (Show, Eq, Functor, Foldable, Traversable) -data ComputedFieldSel v - = CFSScalar !(ComputedFieldScalarSel v) +data ComputedFieldSelect v + = CFSScalar !(ComputedFieldScalarSelect v) | CFSTable !JsonAggSelect !(AnnSimpleSelG v) deriving (Show, Eq) -traverseComputedFieldSel +traverseComputedFieldSelect :: (Applicative f) => (v -> f w) - -> ComputedFieldSel v -> f (ComputedFieldSel w) -traverseComputedFieldSel fv = \case + -> ComputedFieldSelect v -> f (ComputedFieldSelect w) +traverseComputedFieldSelect fv = \case CFSScalar scalarSel -> CFSScalar <$> traverse fv scalarSel - CFSTable b tableSel -> CFSTable b <$> traverseAnnSimpleSel fv tableSel + CFSTable b tableSel -> CFSTable b <$> traverseAnnSimpleSelect fv tableSel type Fields a = [(FieldName, a)] -data ArrSelG v - = ASSimple !(ArrRelG v) - | ASAgg !(ArrRelAggG v) - | ASConnection !(ArrRelConnection v) +data ArraySelectG v + = ASSimple !(ArrayRelationSelectG v) + | ASAggregate !(ArrayAggregateSelectG v) + | ASConnection !(ArrayConnectionSelect v) deriving (Show, Eq) -traverseArrSel +traverseArraySelect :: (Applicative f) => (a -> f b) - -> ArrSelG a - -> f (ArrSelG b) -traverseArrSel f = \case - ASSimple arrRel -> ASSimple <$> traverse (traverseAnnSimpleSel f) arrRel - ASAgg arrRelAgg -> ASAgg <$> traverse (traverseAnnAggSel f) arrRelAgg + -> ArraySelectG a + -> f (ArraySelectG b) +traverseArraySelect f = \case + ASSimple arrRel -> + ASSimple <$> traverse (traverseAnnSimpleSelect f) arrRel + ASAggregate arrRelAgg -> + ASAggregate <$> traverse (traverseAnnAggregateSelect f) arrRelAgg ASConnection relConnection -> ASConnection <$> traverse (traverseConnectionSelect f) relConnection -type ArrSel = ArrSelG S.SQLExp +type ArraySelect = ArraySelectG S.SQLExp -type ArrSelFldsG v = Fields (ArrSelG v) +type ArraySelectFieldsG v = Fields (ArraySelectG v) -data ColOp - = ColOp +data ColumnOp + = ColumnOp { _colOp :: S.SQLOp , _colExp :: S.SQLExp } deriving (Show, Eq) -data AnnColField - = AnnColField +data AnnColumnField + = AnnColumnField { _acfInfo :: !PGColumnInfo , _acfAsText :: !Bool -- ^ If this field is 'True', columns are explicitly casted to @text@ when fetched, which avoids -- an issue that occurs because we don’t currently have proper support for array types. See -- https://github.com/hasura/graphql-engine/pull/3198 for more details. - , _acfOp :: !(Maybe ColOp) + , _acfOp :: !(Maybe ColumnOp) } deriving (Show, Eq) -data AnnFldG v - = FCol !AnnColField - | FObj !(ObjSelG v) - | FArr !(ArrSelG v) - | FComputedField !(ComputedFieldSel v) - | FExp !T.Text +data AnnFieldG v + = AFColumn !AnnColumnField + | AFObjectRelation !(ObjectRelationSelectG v) + | AFArrayRelation !(ArraySelectG v) + | AFComputedField !(ComputedFieldSelect v) + | AFExpression !T.Text deriving (Show, Eq) -mkAnnColField :: PGColumnInfo -> Maybe ColOp -> AnnFldG v -mkAnnColField ci colOpM = - FCol $ AnnColField ci False colOpM +mkAnnColumnField :: PGColumnInfo -> Maybe ColumnOp -> AnnFieldG v +mkAnnColumnField ci colOpM = + AFColumn $ AnnColumnField ci False colOpM -mkAnnColFieldAsText :: PGColumnInfo -> AnnFldG v -mkAnnColFieldAsText ci = - FCol $ AnnColField ci True Nothing +mkAnnColumnFieldAsText :: PGColumnInfo -> AnnFieldG v +mkAnnColumnFieldAsText ci = + AFColumn $ AnnColumnField ci True Nothing -traverseAnnFld +traverseAnnField :: (Applicative f) - => (a -> f b) -> AnnFldG a -> f (AnnFldG b) -traverseAnnFld f = \case - FCol colFld -> pure $ FCol colFld - FObj sel -> FObj <$> traverse (traverseAnnSimpleSel f) sel - FArr sel -> FArr <$> traverseArrSel f sel - FComputedField sel -> FComputedField <$> traverseComputedFieldSel f sel - FExp t -> FExp <$> pure t - -type AnnFld = AnnFldG S.SQLExp - -data TableArgsG v - = TableArgs - { _taWhere :: !(Maybe (AnnBoolExp v)) - , _taOrderBy :: !(Maybe (NE.NonEmpty (AnnOrderByItemG v))) - , _taLimit :: !(Maybe Int) - , _taOffset :: !(Maybe S.SQLExp) - , _taDistCols :: !(Maybe (NE.NonEmpty PGCol)) + => (a -> f b) -> AnnFieldG a -> f (AnnFieldG b) +traverseAnnField f = \case + AFColumn colFld -> pure $ AFColumn colFld + AFObjectRelation sel -> AFObjectRelation <$> traverse (traverseAnnSimpleSelect f) sel + AFArrayRelation sel -> AFArrayRelation <$> traverseArraySelect f sel + AFComputedField sel -> AFComputedField <$> traverseComputedFieldSelect f sel + AFExpression t -> AFExpression <$> pure t + +type AnnField = AnnFieldG S.SQLExp + +data SelectArgsG v + = SelectArgs + { _saWhere :: !(Maybe (AnnBoolExp v)) + , _saOrderBy :: !(Maybe (NE.NonEmpty (AnnOrderByItemG v))) + , _saLimit :: !(Maybe Int) + , _saOffset :: !(Maybe S.SQLExp) + , _saDistinct :: !(Maybe (NE.NonEmpty PGCol)) } deriving (Show, Eq, Generic) -instance (Hashable v) => Hashable (TableArgsG v) +instance (Hashable v) => Hashable (SelectArgsG v) -traverseTableArgs +traverseSelectArgs :: (Applicative f) - => (a -> f b) -> TableArgsG a -> f (TableArgsG b) -traverseTableArgs f (TableArgs wh ordBy lmt ofst distCols) = - TableArgs + => (a -> f b) -> SelectArgsG a -> f (SelectArgsG b) +traverseSelectArgs f (SelectArgs wh ordBy lmt ofst distCols) = + SelectArgs <$> traverse (traverseAnnBoolExp f) wh -- traversing through maybe -> nonempty -> annorderbyitem <*> traverse (traverse (traverseAnnOrderByItem f)) ordBy @@ -214,43 +220,43 @@ traverseTableArgs f (TableArgs wh ordBy lmt ofst distCols) = <*> pure ofst <*> pure distCols -type TableArgs = TableArgsG S.SQLExp +type SelectArgs = SelectArgsG S.SQLExp -noTableArgs :: TableArgsG v -noTableArgs = TableArgs Nothing Nothing Nothing Nothing Nothing +noSelectArgs :: SelectArgsG v +noSelectArgs = SelectArgs Nothing Nothing Nothing Nothing Nothing data PGColFld = PCFCol !PGCol | PCFExp !T.Text deriving (Show, Eq) -type ColFlds = Fields PGColFld +type ColumnFields = Fields PGColFld -data AggOp - = AggOp - { _aoOp :: !T.Text - , _aoFlds :: !ColFlds +data AggregateOp + = AggregateOp + { _aoOp :: !T.Text + , _aoFields :: !ColumnFields } deriving (Show, Eq) -data AggFld +data AggregateField = AFCount !S.CountType - | AFOp !AggOp + | AFOp !AggregateOp | AFExp !T.Text deriving (Show, Eq) -type AggFlds = Fields AggFld -type AnnFldsG v = Fields (AnnFldG v) +type AggregateFields = Fields AggregateField +type AnnFieldsG v = Fields (AnnFieldG v) -traverseAnnFlds +traverseAnnFields :: (Applicative f) - => (a -> f b) -> AnnFldsG a -> f (AnnFldsG b) -traverseAnnFlds f = traverse (traverse (traverseAnnFld f)) + => (a -> f b) -> AnnFieldsG a -> f (AnnFieldsG b) +traverseAnnFields f = traverse (traverse (traverseAnnField f)) -type AnnFlds = AnnFldsG S.SQLExp +type AnnFields = AnnFieldsG S.SQLExp -data TableAggFldG v - = TAFAgg !AggFlds - | TAFNodes !(AnnFldsG v) +data TableAggregateFieldG v + = TAFAgg !AggregateFields + | TAFNodes !(AnnFieldsG v) | TAFExp !T.Text deriving (Show, Eq) @@ -266,7 +272,7 @@ type PageInfoFields = Fields PageInfoField data EdgeField v = EdgeTypename !Text | EdgeCursor - | EdgeNode !(AnnFldsG v) + | EdgeNode !(AnnFieldsG v) deriving (Show, Eq) type EdgeFields v = Fields (EdgeField v) @@ -276,7 +282,7 @@ traverseEdgeField traverseEdgeField f = \case EdgeTypename t -> pure $ EdgeTypename t EdgeCursor -> pure EdgeCursor - EdgeNode fields -> EdgeNode <$> traverseAnnFlds f fields + EdgeNode fields -> EdgeNode <$> traverseAnnFields f fields data ConnectionField v = ConnectionTypename !Text @@ -294,17 +300,17 @@ traverseConnectionField f = \case ConnectionEdges fields -> ConnectionEdges <$> traverse (traverse (traverseEdgeField f)) fields -traverseTableAggFld +traverseTableAggregateField :: (Applicative f) - => (a -> f b) -> TableAggFldG a -> f (TableAggFldG b) -traverseTableAggFld f = \case + => (a -> f b) -> TableAggregateFieldG a -> f (TableAggregateFieldG b) +traverseTableAggregateField f = \case TAFAgg aggFlds -> pure $ TAFAgg aggFlds - TAFNodes annFlds -> TAFNodes <$> traverseAnnFlds f annFlds + TAFNodes annFlds -> TAFNodes <$> traverseAnnFields f annFlds TAFExp t -> pure $ TAFExp t -type TableAggFld = TableAggFldG S.SQLExp -type TableAggFldsG v = Fields (TableAggFldG v) -type TableAggFlds = TableAggFldsG S.SQLExp +type TableAggregateField = TableAggregateFieldG S.SQLExp +type TableAggregateFieldsG v = Fields (TableAggregateFieldG v) +type TableAggregateFields = TableAggregateFieldsG S.SQLExp data ArgumentExp a = AETableRow !(Maybe Iden) -- ^ table row accessor @@ -349,56 +355,89 @@ noTablePermissions = type TablePerm = TablePermG S.SQLExp -data AnnSelG a v - = AnnSelG +data AnnSelectG a v + = AnnSelectG { _asnFields :: !a , _asnFrom :: !(SelectFromG v) , _asnPerm :: !(TablePermG v) - , _asnArgs :: !(TableArgsG v) + , _asnArgs :: !(SelectArgsG v) , _asnStrfyNum :: !Bool } deriving (Show, Eq) -getPermLimit :: AnnSelG a v -> Maybe Int -getPermLimit = _tpLimit . _asnPerm - -traverseAnnSimpleSel +traverseAnnSimpleSelect :: (Applicative f) => (a -> f b) -> AnnSimpleSelG a -> f (AnnSimpleSelG b) -traverseAnnSimpleSel f = traverseAnnSel (traverseAnnFlds f) f +traverseAnnSimpleSelect f = traverseAnnSelect (traverseAnnFields f) f -traverseAnnAggSel +traverseAnnAggregateSelect :: (Applicative f) => (a -> f b) - -> AnnAggSelG a -> f (AnnAggSelG b) -traverseAnnAggSel f = - traverseAnnSel (traverse (traverse (traverseTableAggFld f))) f + -> AnnAggregateSelectG a -> f (AnnAggregateSelectG b) +traverseAnnAggregateSelect f = + traverseAnnSelect (traverse (traverse (traverseTableAggregateField f))) f -traverseAnnSel +traverseAnnSelect :: (Applicative f) => (a -> f b) -> (v -> f w) - -> AnnSelG a v -> f (AnnSelG b w) -traverseAnnSel f1 f2 (AnnSelG flds tabFrom perm args strfyNum) = - AnnSelG + -> AnnSelectG a v -> f (AnnSelectG b w) +traverseAnnSelect f1 f2 (AnnSelectG flds tabFrom perm args strfyNum) = + AnnSelectG <$> f1 flds <*> traverse f2 tabFrom <*> traverseTablePerm f2 perm - <*> traverseTableArgs f2 args + <*> traverseSelectArgs f2 args <*> pure strfyNum -type AnnSimpleSelG v = AnnSelG (AnnFldsG v) v +type AnnSimpleSelG v = AnnSelectG (AnnFieldsG v) v type AnnSimpleSel = AnnSimpleSelG S.SQLExp -type AnnAggSelG v = AnnSelG (TableAggFldsG v) v -type AnnAggSel = AnnAggSelG S.SQLExp +type AnnAggregateSelectG v = AnnSelectG (TableAggregateFieldsG v) v +type AnnAggregateSelect = AnnAggregateSelectG S.SQLExp + +data ConnectionSlice + = SliceFirst !Int + | SliceLast !Int + deriving (Show, Eq, Generic) +instance Hashable ConnectionSlice + +data ConnectionSplitKind + = CSKBefore + | CSKAfter + deriving (Show, Eq, Generic) +instance Hashable ConnectionSplitKind + +data ConnectionSplit v + = ConnectionSplit + { _csKind :: !ConnectionSplitKind + , _csValue :: !v + , _csOrderBy :: !(OrderByItemG (AnnOrderByElementG ())) + } deriving (Show, Eq, Functor, Generic, Foldable, Traversable) +instance (Hashable v) => Hashable (ConnectionSplit v) + +traverseConnectionSplit + :: (Applicative f) + => (a -> f b) -> ConnectionSplit a -> f (ConnectionSplit b) +traverseConnectionSplit f (ConnectionSplit k v ob) = + ConnectionSplit k <$> f v <*> pure ob + +data ConnectionSelect v + = ConnectionSelect + { _csPrimaryKeyColumns :: !(NE.NonEmpty PGColumnInfo) + , _csSplit :: !(Maybe (NE.NonEmpty (ConnectionSplit v))) + , _csSlice :: !(Maybe ConnectionSlice) + , _csSelect :: !(AnnSelectG (ConnectionFields v) v) + } deriving (Show, Eq) -type ConnectionSelect v = AnnSelG (ConnectionFields v) v traverseConnectionSelect :: (Applicative f) => (a -> f b) -> ConnectionSelect a -> f (ConnectionSelect b) -traverseConnectionSelect f = - traverseAnnSel (traverse (traverse (traverseConnectionField f))) f +traverseConnectionSelect f (ConnectionSelect pkCols cSplit cSlice sel) = + ConnectionSelect pkCols + <$> traverse (traverse (traverseConnectionSplit f)) cSplit + <*> pure cSlice + <*> traverseAnnSelect (traverse (traverse (traverseConnectionField f))) f sel data FunctionArgsExpG a = FunctionArgsExp @@ -429,111 +468,102 @@ insertFunctionArg argName index value (FunctionArgsExp positional named) = where insertAt i a = toList . Seq.insertAt i a . Seq.fromList -data BaseNode - = BaseNode - { _bnPrefix :: !Iden - , _bnDistinct :: !(Maybe S.DistinctExpr) - , _bnFrom :: !S.FromItem - , _bnWhere :: !S.BoolExp - , _bnOrderBy :: !(Maybe S.OrderByExp) - , _bnLimit :: !(Maybe Int) - , _bnOffset :: !(Maybe S.SQLExp) - - , _bnExtrs :: !(HM.HashMap S.Alias S.SQLExp) - , _bnObjs :: !(HM.HashMap RelName ObjNode) - , _bnArrs :: !(HM.HashMap S.Alias ArrNode) - , _bnComputedFieldTables :: !(HM.HashMap FieldName CFTableNode) - } deriving (Show, Eq) - -mergeBaseNodes :: BaseNode -> BaseNode -> BaseNode -mergeBaseNodes lNodeDet rNodeDet = - BaseNode pfx dExp f whr ordBy limit offset - (HM.union lExtrs rExtrs) - (HM.unionWith mergeObjNodes lObjs rObjs) - (HM.unionWith mergeArrNodes lArrs rArrs) - (HM.unionWith mergeCFTableNodes lCFTables rCFTables) - where - BaseNode pfx dExp f whr ordBy limit offset lExtrs lObjs lArrs lCFTables - = lNodeDet - BaseNode _ _ _ _ _ _ _ rExtrs rObjs rArrs rCFTables - = rNodeDet - -data OrderByNode - = OBNNothing - | OBNObjNode !RelName !ObjNode - | OBNArrNode !S.Alias !ArrNode - deriving (Show, Eq) +data SourcePrefixes + = SourcePrefixes + { _pfThis :: !Iden -- ^ Current source prefix + , _pfBase :: !Iden + -- ^ Base table source row identifier to generate + -- the table's column identifiers for computed field + -- function input parameters + } deriving (Show, Eq, Generic) +instance Hashable SourcePrefixes + +data SelectSource + = SelectSource + { _ssPrefix :: !Iden + , _ssFrom :: !S.FromItem + , _ssDistinct :: !(Maybe S.DistinctExpr) + , _ssWhere :: !S.BoolExp + , _ssOrderBy :: !(Maybe S.OrderByExp) + , _ssLimit :: !(Maybe Int) + , _ssOffset :: !(Maybe S.SQLExp) + } deriving (Show, Eq, Generic) +instance Hashable SelectSource -data ArrRelCtxG v - = ArrRelCtx - { aacFields :: !(ArrSelFldsG v) - , aacAggOrdBys :: ![RelName] +data SelectNode + = SelectNode + { _snExtractors :: !(HM.HashMap S.Alias S.SQLExp) + , _snJoinTree :: !JoinTree } deriving (Show, Eq) -type ArrRelCtx = ArrRelCtxG S.SQLExp +instance Semigroup SelectNode where + SelectNode lExtrs lJoinTree <> SelectNode rExtrs rJoinTree = + SelectNode (lExtrs <> rExtrs) (lJoinTree <> rJoinTree) -emptyArrRelCtx :: ArrRelCtxG a -emptyArrRelCtx = ArrRelCtx [] [] - -data ArrNodeItemG v - = ANIField !(FieldName, ArrSelG v) - | ANIAggOrdBy !RelName - deriving (Show, Eq) +data ObjectRelationSource + = ObjectRelationSource + { _orsRelationshipName :: !RelName + , _orsRelationMapping :: !(HM.HashMap PGCol PGCol) + , _orsSelectSource :: !SelectSource + } deriving (Show, Eq, Generic) +instance Hashable ObjectRelationSource -type ArrNodeItem = ArrNodeItemG S.SQLExp +data ArrayRelationSource + = ArrayRelationSource + { _arsAlias :: !S.Alias + , _arsRelationMapping :: !(HM.HashMap PGCol PGCol) + , _arsSelectSource :: !SelectSource + } deriving (Show, Eq, Generic) +instance Hashable ArrayRelationSource -data ObjNode - = ObjNode - { _rnRelMapping :: !(HashMap PGCol PGCol) - , _rnNodeDet :: !BaseNode +data ArraySelectNode + = ArraySelectNode + { _asnTopExtractors :: ![S.Extractor] + , _asnSelectNode :: !SelectNode } deriving (Show, Eq) -mergeObjNodes :: ObjNode -> ObjNode -> ObjNode -mergeObjNodes lNode rNode = - ObjNode colMapping $ mergeBaseNodes lBN rBN - where - ObjNode colMapping lBN = lNode - ObjNode _ rBN = rNode - --- simple array select, aggregate select and order by --- nodes differ in extractors -data ArrNode - = ArrNode - { _anExtr :: ![S.Extractor] - , _anRelMapping :: !(HashMap PGCol PGCol) - , _anNodeDet :: !BaseNode - } deriving (Show, Eq) +instance Semigroup ArraySelectNode where + ArraySelectNode lTopExtrs lSelNode <> ArraySelectNode rTopExtrs rSelNode = + ArraySelectNode (lTopExtrs <> rTopExtrs) (lSelNode <> rSelNode) -mergeArrNodes :: ArrNode -> ArrNode -> ArrNode -mergeArrNodes lNode rNode = - ArrNode (lExtrs `union` rExtrs) colMapping $ - mergeBaseNodes lBN rBN - where - ArrNode lExtrs colMapping lBN = lNode - ArrNode rExtrs _ rBN = rNode - -data ArrNodeInfo - = ArrNodeInfo - { _aniAlias :: !S.Alias - , _aniPrefix :: !Iden - , _aniSubQueryRequired :: !Bool - } deriving (Show, Eq) +data ComputedFieldTableSetSource + = ComputedFieldTableSetSource + { _cftssFieldName :: !FieldName + , _cftssSelectType :: !JsonAggSelect + , _cftssSelectSource :: !SelectSource + } deriving (Show, Eq, Generic) +instance Hashable ComputedFieldTableSetSource + +data ArrayConnectionSource + = ArrayConnectionSource + { _acsAlias :: !S.Alias + , _acsRelationMapping :: !(HM.HashMap PGCol PGCol) + , _acsSplitFilter :: !(Maybe S.BoolExp) + , _acsSlice :: !(Maybe ConnectionSlice) + , _acsSource :: !SelectSource + } deriving (Show, Eq, Generic) --- | Node for computed field returning setof -data CFTableNode - = CFTableNode - { _ctnSelectType :: !JsonAggSelect - , _ctnNode :: !BaseNode +instance Hashable ArrayConnectionSource + +data JoinTree + = JoinTree + { _jtObjectRelations :: !(HM.HashMap ObjectRelationSource SelectNode) + , _jtArrayRelations :: !(HM.HashMap ArrayRelationSource ArraySelectNode) + , _jtArrayConnections :: !(HM.HashMap ArrayConnectionSource ArraySelectNode) + , _jtComputedFieldTableSets :: !(HM.HashMap ComputedFieldTableSetSource SelectNode) } deriving (Show, Eq) -mergeCFTableNodes :: CFTableNode -> CFTableNode -> CFTableNode -mergeCFTableNodes lNode rNode = - CFTableNode - (_ctnSelectType rNode) - (mergeBaseNodes (_ctnNode lNode) (_ctnNode rNode)) +instance Semigroup JoinTree where + JoinTree lObjs lArrs lArrConns lCfts <> JoinTree rObjs rArrs rArrConns rCfts = + JoinTree (HM.unionWith (<>) lObjs rObjs) + (HM.unionWith (<>) lArrs rArrs) + (HM.unionWith (<>) lArrConns rArrConns) + (HM.unionWith (<>) lCfts rCfts) -data Prefixes - = Prefixes - { _pfThis :: !Iden -- Current node prefix - , _pfBase :: !Iden -- Base table row identifier for computed field function - } deriving (Show, Eq) +instance Monoid JoinTree where + mempty = JoinTree mempty mempty mempty mempty + +data PermissionLimitSubQuery + = PLSQRequired !Int -- ^ Permission limit + | PLSQNotRequired + deriving (Show, Eq) diff --git a/server/src-lib/Hasura/RQL/Types/Common.hs b/server/src-lib/Hasura/RQL/Types/Common.hs index 9c70b544826b7..b685bc9df7473 100644 --- a/server/src-lib/Hasura/RQL/Types/Common.hs +++ b/server/src-lib/Hasura/RQL/Types/Common.hs @@ -150,6 +150,7 @@ newtype FieldName deriving ( Show, Eq, Ord, Hashable, FromJSON, ToJSON , FromJSONKey, ToJSONKey, Lift, Data, Generic , IsString, Arbitrary, NFData, Cacheable + , Semigroup ) instance IsIden FieldName where diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs index 8e1b3a67117f9..7e291559bfda1 100644 --- a/server/src-lib/Hasura/SQL/DML.hs +++ b/server/src-lib/Hasura/SQL/DML.hs @@ -59,7 +59,7 @@ instance ToSQL OffsetExp where "OFFSET" <-> toSQL se newtype OrderByExp - = OrderByExp [OrderByItem] + = OrderByExp (NonEmpty OrderByItem) deriving (Show, Eq, NFData, Data, Cacheable, Hashable) data OrderByItem @@ -100,7 +100,7 @@ instance ToSQL NullsOrder where instance ToSQL OrderByExp where toSQL (OrderByExp l) = - "ORDER BY" <-> (", " <+> l) + "ORDER BY" <-> (", " <+> toList l) newtype GroupByExp = GroupByExp [SQLExp] @@ -306,6 +306,7 @@ data SQLExp | SEBool !BoolExp | SEExcluded !Iden | SEArray ![SQLExp] + | SEArrayIndex !SQLExp !SQLExp | SETuple !TupleExp | SECount !CountType | SENamedArg !Iden !SQLExp @@ -375,6 +376,9 @@ instance ToSQL SQLExp where <> toSQL i toSQL (SEArray exps) = "ARRAY" <> TB.char '[' <> (", " <+> exps) <> TB.char ']' + toSQL (SEArrayIndex arrayExp indexExp) = + paren (toSQL arrayExp) + <> TB.char '[' <> toSQL indexExp <> TB.char ']' toSQL (SETuple tup) = toSQL tup toSQL (SECount ty) = "COUNT" <> paren (toSQL ty) -- https://www.postgresql.org/docs/current/sql-syntax-calling-funcs.html @@ -516,6 +520,7 @@ data FromItem | FIFunc !FunctionExp | FIUnnest ![SQLExp] !Alias ![SQLExp] | FISelect !Lateral !Select !Alias + | FISelectWith !Lateral !(SelectWithG Select) !Alias | FIValues !ValuesExp !Alias !(Maybe [PGCol]) | FIJoin !JoinExpr deriving (Show, Eq, Generic, Data) @@ -544,6 +549,8 @@ instance ToSQL FromItem where "UNNEST" <> paren (", " <+> args) <-> toSQL als <> paren (", " <+> cols) toSQL (FISelect mla sel al) = toSQL mla <-> paren (toSQL sel) <-> toSQL al + toSQL (FISelectWith mla selWith al) = + toSQL mla <-> paren (toSQL selWith) <-> toSQL al toSQL (FIValues valsExp al mCols) = paren (toSQL valsExp) <-> toSQL al <-> toSQL (toColTupExp <$> mCols) @@ -892,14 +899,20 @@ instance ToSQL CTE where CTEUpdate q -> toSQL q CTEDelete q -> toSQL q -data SelectWith +data SelectWithG v = SelectWith - { swCTEs :: [(Alias, CTE)] + { swCTEs :: ![(Alias, v)] , swSelect :: !Select - } deriving (Show, Eq) + } deriving (Show, Eq, Generic, Data) + +instance (NFData v) => NFData (SelectWithG v) +instance (Cacheable v) => Cacheable (SelectWithG v) +instance (Hashable v) => Hashable (SelectWithG v) -instance ToSQL SelectWith where +instance (ToSQL v) => ToSQL (SelectWithG v) where toSQL (SelectWith ctes sel) = "WITH " <> (", " <+> map f ctes) <-> toSQL sel where f (Alias al, q) = toSQL al <-> "AS" <-> paren (toSQL q) + +type SelectWith = SelectWithG CTE diff --git a/server/src-lib/Hasura/SQL/Rewrite.hs b/server/src-lib/Hasura/SQL/Rewrite.hs index 6235709b1ea18..d7ced56ecc58d 100644 --- a/server/src-lib/Hasura/SQL/Rewrite.hs +++ b/server/src-lib/Hasura/SQL/Rewrite.hs @@ -1,5 +1,6 @@ module Hasura.SQL.Rewrite ( prefixNumToAliases + , prefixNumToAliasesSelectWith ) where import qualified Data.HashMap.Strict as Map @@ -20,6 +21,11 @@ prefixNumToAliases :: S.Select -> S.Select prefixNumToAliases s = uSelect s `evalState` UniqSt 0 Map.empty +prefixNumToAliasesSelectWith + :: S.SelectWithG S.Select -> S.SelectWithG S.Select +prefixNumToAliasesSelectWith s = + uSelectWith s `evalState` UniqSt 0 Map.empty + type Rewrite a = State a data UniqSt @@ -56,6 +62,12 @@ restoringIdens action = do modify' $ \s -> s { _uqIdens = idens } return res +uSelectWith :: S.SelectWithG S.Select -> Uniq (S.SelectWithG S.Select) +uSelectWith (S.SelectWith ctes baseSelect) = + S.SelectWith + <$> forM ctes (\(als, sel) -> (als,) <$> restoringIdens (uSelect sel)) + <*> uSelect baseSelect + uSelect :: S.Select -> Uniq S.Select uSelect sel = do -- this has to be the first thing to process @@ -113,6 +125,10 @@ uFromItem fromItem = case fromItem of newSel <- restoringIdens $ uSelect sel newAls <- addAlias al return $ S.FISelect isLateral newSel newAls + S.FISelectWith isLateral selectWith al -> do + newSelectWith <- uSelectWith selectWith + newAls <- addAlias al + return $ S.FISelectWith isLateral newSelectWith newAls S.FIValues (S.ValuesExp tups) als mCols -> do newValExp <- fmap S.ValuesExp $ forM tups $ \(S.TupleExp ts) -> @@ -196,8 +212,10 @@ uSqlExp = restoringIdens . \case S.SEExcluded <$> return t S.SEArray l -> S.SEArray <$> mapM uSqlExp l + S.SEArrayIndex arrayExp indexExp -> + S.SEArrayIndex <$> uSqlExp arrayExp <*> uSqlExp indexExp S.SETuple (S.TupleExp l) -> - S.SEArray <$> mapM uSqlExp l + S.SETuple . S.TupleExp <$> mapM uSqlExp l S.SECount cty -> return $ S.SECount cty S.SENamedArg arg val -> S.SENamedArg arg <$> uSqlExp val S.SEFunction funcExp -> S.SEFunction <$> uFunctionExp funcExp diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index 72d55e55c1aa9..00a079b8efbfc 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -5,6 +5,7 @@ import Hasura.Prelude import Control.Lens ((^..)) import Data.Aeson +import Data.Aeson.Internal import Data.Char import Data.List (find) import Language.Haskell.TH.Syntax (Lift, Q, TExp) @@ -20,6 +21,7 @@ import qualified Data.Text as T import qualified Data.Text.IO as TI import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID +import qualified Data.Vector as V import qualified Language.Haskell.TH.Syntax as TH import qualified Network.HTTP.Client as HC import qualified Network.HTTP.Types as HTTP @@ -227,3 +229,16 @@ makeReasonMessage errors showError = [singleError] -> "because " <> showError singleError _ -> "for the following reasons:\n" <> T.unlines (map ((" • " <>) . showError) errors) + +executeJSONPath :: JSONPath -> Value -> IResult Value +executeJSONPath jsonPath = iparse (valueParser jsonPath) + where + valueParser path value = case path of + [] -> pure value + (pathElement:remaining) -> parseWithPathElement pathElement value >>= + (( pathElement) . valueParser remaining) + where + parseWithPathElement = \case + Key k -> withObject "Object" (.: k) + Index i -> withArray "Array" $ + maybe (fail "Array index out of range") pure . (V.!? i) From b3dd4fd26b9616bbcc47b8eea269c923703c2672 Mon Sep 17 00:00:00 2001 From: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Date: Mon, 18 May 2020 20:45:24 +0530 Subject: [PATCH 09/29] '/v1/relay' endpoint for relay schema --- server/graphql-engine.cabal | 1 + server/src-lib/Hasura/GraphQL/Execute.hs | 24 +- server/src-lib/Hasura/GraphQL/Explain.hs | 10 +- server/src-lib/Hasura/GraphQL/RelaySchema.hs | 280 ++++++++++++++++++ server/src-lib/Hasura/GraphQL/Schema.hs | 58 +--- .../src-lib/Hasura/GraphQL/Schema/Common.hs | 20 +- .../src-lib/Hasura/GraphQL/Schema/Function.hs | 12 + .../src-lib/Hasura/GraphQL/Schema/Select.hs | 20 +- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 12 +- .../Hasura/GraphQL/Transport/WebSocket.hs | 21 +- server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs | 5 + .../src-lib/Hasura/RQL/Types/SchemaCache.hs | 1 + server/src-lib/Hasura/Server/App.hs | 21 +- 13 files changed, 398 insertions(+), 87 deletions(-) create mode 100644 server/src-lib/Hasura/GraphQL/RelaySchema.hs diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 94a735796ca1a..897d00b150794 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -335,6 +335,7 @@ library , Hasura.GraphQL.Schema.Mutation.Update , Hasura.GraphQL.Schema.Mutation.Delete , Hasura.GraphQL.Schema + , Hasura.GraphQL.RelaySchema , Hasura.GraphQL.Utils , Hasura.GraphQL.Validate , Hasura.GraphQL.Validate.Types diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index d5c613e297837..81b8eec891d40 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -1,5 +1,6 @@ module Hasura.GraphQL.Execute ( GQExecPlan(..) + , GraphQLAPIType(..) , ExecPlanPartial , getExecPlanPartial @@ -39,7 +40,6 @@ import Hasura.EncJSON import Hasura.GraphQL.Context import Hasura.GraphQL.Logging import Hasura.GraphQL.Resolve.Context -import Hasura.GraphQL.Schema import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.GraphQL.Validate.Types import Hasura.HTTP @@ -51,6 +51,7 @@ import Hasura.Server.Utils (RequestId, mkClientHead mkSetCookieHeaders) import Hasura.Server.Version (HasVersion) +import qualified Hasura.GraphQL.Context as GC import qualified Hasura.GraphQL.Execute.LiveQuery as EL import qualified Hasura.GraphQL.Execute.Plan as EP import qualified Hasura.GraphQL.Execute.Query as EQ @@ -60,6 +61,12 @@ import qualified Hasura.GraphQL.Validate.Types as VT import qualified Hasura.Logging as L import qualified Hasura.Server.Telemetry.Counters as Telem +-- The GraphQL API type +data GraphQLAPIType + = GATGeneral + | GATRelay + deriving (Show, Eq) + -- The current execution plan of a graphql operation, it is -- currently, either local pg execution or a remote execution -- @@ -123,15 +130,19 @@ getExecPlanPartial :: (MonadReusability m, MonadError QErr m) => UserInfo -> SchemaCache + -> GraphQLAPIType -> Bool -> GQLReqParsed -> m ExecPlanPartial -getExecPlanPartial userInfo sc enableAL req = do +getExecPlanPartial userInfo sc apiType enableAL req = do -- check if query is in allowlist when enableAL checkQueryInAllowlist - gCtx <- flip runCacheRT sc $ getGCtx role gCtxRoleMap + let gCtx = case apiType of + GATGeneral -> Map.lookupDefault defaultRemoteGCtx role gCtxRoleMap + GATRelay -> Map.lookupDefault GC.emptyGCtx role relayGCtxMap + queryParts <- flip runReaderT gCtx $ VQ.getQueryParts req let opDef = VQ.qpOpDef queryParts @@ -153,6 +164,8 @@ getExecPlanPartial userInfo sc enableAL req = do where role = userRole userInfo gCtxRoleMap = scGCtxMap sc + defaultRemoteGCtx = scDefaultRemoteGCtx sc + relayGCtxMap = scRelayGCtxMap sc checkQueryInAllowlist = -- only for non-admin roles @@ -186,12 +199,13 @@ getResolvedExecPlan -> Bool -> SchemaCache -> SchemaCacheVer + -> GraphQLAPIType -> HTTP.Manager -> [N.Header] -> GQLReqUnparsed -> m (Telem.CacheHit, ExecPlanResolved) getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx - enableAL sc scVer httpManager reqHeaders reqUnparsed = do + enableAL sc scVer apiType httpManager reqHeaders reqUnparsed = do planM <- liftIO $ EP.getPlan scVer (userRole userInfo) opNameM queryStr planCache let usrVars = userVars userInfo @@ -212,7 +226,7 @@ getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx noExistingPlan = do req <- toParsed reqUnparsed (partialExecPlan, queryReusability) <- runReusabilityT $ - getExecPlanPartial userInfo sc enableAL req + getExecPlanPartial userInfo sc apiType enableAL req forM partialExecPlan $ \(gCtx, rootSelSet) -> case rootSelSet of VQ.RMutation selSet -> do diff --git a/server/src-lib/Hasura/GraphQL/Explain.hs b/server/src-lib/Hasura/GraphQL/Explain.hs index 991665ffbcef9..4f5df3c571d0e 100644 --- a/server/src-lib/Hasura/GraphQL/Explain.hs +++ b/server/src-lib/Hasura/GraphQL/Explain.hs @@ -28,8 +28,9 @@ import qualified Hasura.SQL.DML as S data GQLExplain = GQLExplain - { _gqeQuery :: !GH.GQLReqParsed - , _gqeUser :: !(Maybe (Map.HashMap Text Text)) + { _gqeQuery :: !GH.GQLReqParsed + , _gqeUser :: !(Maybe (Map.HashMap Text Text)) + , _gqeIsRelay :: !(Maybe Bool) } deriving (Show, Eq) $(J.deriveJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True} @@ -114,9 +115,9 @@ explainGQLQuery -> Bool -> GQLExplain -> m EncJSON -explainGQLQuery pgExecCtx sc sqlGenCtx enableAL (GQLExplain query userVarsRaw) = do +explainGQLQuery pgExecCtx sc sqlGenCtx enableAL (GQLExplain query userVarsRaw maybeIsRelay) = do (execPlan, queryReusability) <- runReusabilityT $ - E.getExecPlanPartial userInfo sc enableAL query + E.getExecPlanPartial userInfo sc apiType enableAL query (gCtx, rootSelSet) <- case execPlan of E.GExPHasura (gCtx, rootSelSet) -> return (gCtx, rootSelSet) @@ -131,6 +132,7 @@ explainGQLQuery pgExecCtx sc sqlGenCtx enableAL (GQLExplain query userVarsRaw) = (plan, _) <- E.getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability rootField runInTx $ encJFromJValue <$> E.explainLiveQueryPlan plan where + apiType = bool E.GATGeneral E.GATRelay $ fromMaybe False maybeIsRelay usrVars = mkUserVars $ maybe [] Map.toList userVarsRaw userInfo = mkUserInfo (fromMaybe adminRole $ roleFromVars usrVars) usrVars runInTx = liftEither <=< liftIO . runExceptT . runLazyTx pgExecCtx Q.ReadOnly diff --git a/server/src-lib/Hasura/GraphQL/RelaySchema.hs b/server/src-lib/Hasura/GraphQL/RelaySchema.hs new file mode 100644 index 0000000000000..5a11dad74f1a8 --- /dev/null +++ b/server/src-lib/Hasura/GraphQL/RelaySchema.hs @@ -0,0 +1,280 @@ +module Hasura.GraphQL.RelaySchema where + +import Control.Lens.Extended hiding (op) + +import qualified Data.HashMap.Strict as Map +import qualified Data.HashSet as Set + +import qualified Data.Text as T + +import Hasura.GraphQL.Context +import Hasura.GraphQL.Resolve.Types +import Hasura.GraphQL.Validate.Types +import Hasura.Prelude +import Hasura.RQL.Types +import Hasura.Server.Utils (duplicates) +import Hasura.SQL.Types + +import Hasura.GraphQL.Schema +import Hasura.GraphQL.Schema.BoolExp +import Hasura.GraphQL.Schema.Builder +import Hasura.GraphQL.Schema.Common +import Hasura.GraphQL.Schema.Function +import Hasura.GraphQL.Schema.OrderBy +import Hasura.GraphQL.Schema.Select + +mkRelayGCtxMap + :: forall m. (MonadError QErr m) + => TableCache -> FunctionCache -> m GCtxMap +mkRelayGCtxMap tableCache functionCache = do + typesMapL <- mapM (mkRelayGCtxMapTable tableCache functionCache) $ + filter (tableFltr . _tiCoreInfo) $ Map.elems tableCache + typesMap <- combineTypes typesMapL + let gCtxMap = flip Map.map typesMap $ + \(ty, flds) -> mkGCtx ty flds mempty + return gCtxMap + where + tableFltr ti = not (isSystemDefined $ _tciSystemDefined ti) && isValidObjectName (_tciName ti) + + combineTypes + :: [Map.HashMap RoleName (TyAgg, RootFields)] + -> m (Map.HashMap RoleName (TyAgg, RootFields)) + combineTypes maps = do + let listMap = foldr (Map.unionWith (++) . Map.map pure) mempty maps + flip Map.traverseWithKey listMap $ \_ typeList -> do + let tyAgg = mconcat $ map (^. _1) typeList + rootFields <- combineRootFields $ map (^. _2) typeList + pure (tyAgg, rootFields) + + combineRootFields :: [RootFields] -> m RootFields + combineRootFields rootFields = do + let duplicateQueryFields = duplicates $ + concatMap (Map.keys . _rootQueryFields) rootFields + duplicateMutationFields = duplicates $ + concatMap (Map.keys . _rootMutationFields) rootFields + + -- TODO: The following exception should result in inconsistency + when (not $ null duplicateQueryFields) $ + throw400 Unexpected $ "following query root fields are duplicated: " + <> showNames duplicateQueryFields + + when (not $ null duplicateMutationFields) $ + throw400 Unexpected $ "following mutation root fields are duplicated: " + <> showNames duplicateMutationFields + + pure $ mconcat rootFields + +mkRelayGCtxMapTable + :: (MonadError QErr m) + => TableCache + -> FunctionCache + -> TableInfo + -> m (Map.HashMap RoleName (TyAgg, RootFields)) +mkRelayGCtxMapTable tableCache funcCache tabInfo = do + m <- flip Map.traverseWithKey rolePerms $ + mkRelayGCtxRole tableCache tn descM fields primaryKey tabFuncs + adminSelFlds <- mkAdminSelFlds fields tableCache + let adminCtx = mkRelayGCtxRole' tn descM (Just (True, adminSelFlds)) + primaryKey tabFuncs + return $ Map.insert adminRole (adminCtx, adminRootFlds) m + where + TableInfo coreInfo rolePerms _ = tabInfo + TableCoreInfo tn descM _ fields primaryKey _ _ _ _ _ = coreInfo + tabFuncs = filter (isValidObjectName . fiName) $ + getFuncsOfTable tn funcCache + adminRootFlds = + getRelayRootFldsRole' tn primaryKey fields tabFuncs + (Just (noFilter, Nothing, [], True)) + +mkRelayGCtxRole + :: (MonadError QErr m) + => TableCache + -> QualifiedTable + -> Maybe PGDescription + -> FieldInfoMap FieldInfo + -> Maybe (PrimaryKey PGColumnInfo) + -> [FunctionInfo] + -> RoleName + -> RolePermInfo + -> m (TyAgg, RootFields) +mkRelayGCtxRole tableCache tn descM fields primaryKey funcs role permInfo = do + selPermM <- mapM (getSelPerm tableCache fields role) $ _permSel permInfo + let tyAgg = mkRelayGCtxRole' tn descM selPermM primaryKey funcs + rootFlds = getRelayRootFldsRole' tn primaryKey fields funcs + (mkSel <$> _permSel permInfo) + return (tyAgg, rootFlds) + where + mkSel s = ( spiFilter s, spiLimit s + , spiRequiredHeaders s, spiAllowAgg s + ) + +mkRelayGCtxRole' + :: QualifiedTable + -> Maybe PGDescription + -- ^ Postgres description + -> Maybe (Bool, [SelField]) + -- ^ select permission + -> Maybe (PrimaryKey PGColumnInfo) + -> [FunctionInfo] + -- ^ all functions + -> TyAgg +mkRelayGCtxRole' tn descM selPermM pkeyCols funcs = + TyAgg (mkTyInfoMap allTypes) fieldMap scalars ordByCtx + where + ordByCtx = fromMaybe Map.empty ordByCtxM + + funcInpArgTys = bool [] (map TIInpObj funcArgInpObjs) $ isJust selFldsM + + allTypes = queryTypes <> aggQueryTypes <> funcInpArgTys <> computedFieldFuncArgsInps + + queryTypes = map TIObj selectObjects <> + catMaybes + [ TIInpObj <$> boolExpInpObjM + , TIInpObj <$> ordByInpObjM + , TIEnum <$> selColInpTyM + ] + aggQueryTypes = map TIObj aggObjs <> map TIInpObj aggOrdByInps + + fieldMap = Map.unions $ catMaybes [boolExpInpObjFldsM, selObjFldsM] + scalars = selByPkScalarSet <> funcArgScalarSet <> computedFieldFuncArgScalars + + selFldsM = snd <$> selPermM + selColNamesM = (map pgiName . getPGColumnFields) <$> selFldsM + selColInpTyM = mkSelColumnTy tn <$> selColNamesM + -- boolexp input type + boolExpInpObjM = (mkBoolExpInp tn) <$> selFldsM + -- funcargs input type + funcArgInpObjs = flip mapMaybe funcs $ \func -> + mkFuncArgsInp (fiName func) (getInputArgs func) + -- funcArgCtx = Map.unions funcArgCtxs + funcArgScalarSet = funcs ^.. folded.to getInputArgs.folded.to (_qptName.faType) + + -- helper + mkFldMap ty = Map.fromList . concatMap (mkFld ty) + mkFld ty = \case + SFPGColumn ci -> [((ty, pgiName ci), RFPGColumn ci)] + SFRelationship (RelationshipFieldInfo relInfo allowAgg cols permFilter permLimit maybePkCols _) -> + let relationshipName = riName relInfo + relFld = ( (ty, mkRelName relationshipName) + , RFRelationship $ RelationshipField relInfo RFKSimple cols permFilter permLimit + ) + aggRelFld = ( (ty, mkAggRelName relationshipName) + , RFRelationship $ RelationshipField relInfo RFKAggregate cols permFilter permLimit + ) + maybeConnFld = maybePkCols <&> \pkCols -> + ( (ty, mkConnectionRelName relationshipName) + , RFRelationship $ RelationshipField relInfo + (RFKConnection pkCols) cols permFilter permLimit + ) + in case riType relInfo of + ObjRel -> [relFld] + ArrRel -> bool [relFld] ([relFld, aggRelFld] <> maybe [] pure maybeConnFld) allowAgg + SFComputedField cf -> pure + ( (ty, mkComputedFieldName $ _cfName cf) + , RFComputedField cf + ) + + -- the fields used in bool exp + boolExpInpObjFldsM = mkFldMap (mkBoolExpTy tn) <$> selFldsM + + -- table obj + selectObjects = case selPermM of + Just (_, selFlds) -> + [ mkTableObj tn True descM selFlds + , mkTableEdgeObj tn + , mkTableConnectionObj tn + ] + Nothing -> [] + + -- aggregate objs and order by inputs + (aggObjs, aggOrdByInps) = case selPermM of + Just (True, selFlds) -> + let cols = getPGColumnFields selFlds + numCols = onlyNumCols cols + compCols = onlyComparableCols cols + objs = [ mkTableAggObj tn + , mkTableAggregateFieldsObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) + ] <> mkColAggregateFieldsObjs selFlds + ordByInps = mkTabAggOrdByInpObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) + : mkTabAggregateOpOrdByInpObjs tn (numCols, numAggregateOps) (compCols, compAggregateOps) + in (objs, ordByInps) + _ -> ([], []) + + getNumericCols = onlyNumCols . getPGColumnFields + getComparableCols = onlyComparableCols . getPGColumnFields + onlyFloat = const $ mkScalarTy PGFloat + + mkTypeMaker "sum" = mkColumnType + mkTypeMaker _ = onlyFloat + + mkColAggregateFieldsObjs flds = + let numCols = getNumericCols flds + compCols = getComparableCols flds + mkNumObjFld n = mkTableColAggregateFieldsObj tn n (mkTypeMaker n) numCols + mkCompObjFld n = mkTableColAggregateFieldsObj tn n mkColumnType compCols + numFldsObjs = bool (map mkNumObjFld numAggregateOps) [] $ null numCols + compFldsObjs = bool (map mkCompObjFld compAggregateOps) [] $ null compCols + in numFldsObjs <> compFldsObjs + -- the fields used in table object + selObjFldsM = mkFldMap (mkTableTy tn) <$> selFldsM + -- the scalar set for table_by_pk arguments + selByPkScalarSet = pkeyCols ^.. folded.to _pkColumns.folded.to pgiType._PGColumnScalar + + ordByInpCtxM = mkOrdByInpObj tn <$> selFldsM + (ordByInpObjM, ordByCtxM) = case ordByInpCtxM of + Just (a, b) -> (Just a, Just b) + Nothing -> (Nothing, Nothing) + + -- computed fields' function args input objects and scalar types + mkComputedFieldRequiredTypes computedFieldInfo = + let ComputedFieldFunction qf inputArgs _ _ = _cfFunction computedFieldInfo + scalarArgs = map (_qptName . faType) $ toList inputArgs + in (, scalarArgs) <$> mkFuncArgsInp qf inputArgs + + computedFieldReqTypes = catMaybes $ + maybe [] (map mkComputedFieldRequiredTypes . getComputedFields) selFldsM + + computedFieldFuncArgsInps = map (TIInpObj . fst) computedFieldReqTypes + computedFieldFuncArgScalars = Set.fromList $ concatMap snd computedFieldReqTypes + +getRelayRootFldsRole' + :: QualifiedTable + -> Maybe (PrimaryKey PGColumnInfo) + -> FieldInfoMap FieldInfo + -> [FunctionInfo] + -> Maybe (AnnBoolExpPartialSQL, Maybe Int, [T.Text], Bool) -- select filter + -> RootFields +getRelayRootFldsRole' tn primaryKey fields funcs selM = + RootFields + { _rootQueryFields = makeFieldMap $ + funcConnectionQueries + <> catMaybes + [ getSelConnectionDet <$> selM <*> maybePrimaryKeyColumns + ] + , _rootMutationFields = mempty + } + where + maybePrimaryKeyColumns = fmap _pkColumns primaryKey + makeFieldMap = mapFromL (_fiName . snd) + colGNameMap = mkPGColGNameMap $ getCols fields + + funcConnectionQueries = fromMaybe [] $ getFuncQueryConnectionFlds + <$> selM <*> maybePrimaryKeyColumns + + getSelConnectionDet (selFltr, pLimit, hdrs, _) primaryKeyColumns = + selFldHelper (QCSelectConnection primaryKeyColumns) + (mkSelFldConnection Nothing) selFltr pLimit hdrs + + selFldHelper f g pFltr pLimit hdrs = + ( f $ SelOpCtx tn hdrs colGNameMap pFltr pLimit + , g tn + ) + + getFuncQueryConnectionFlds (selFltr, pLimit, hdrs, _) primaryKeyColumns = + funcFldHelper (QCFuncConnection primaryKeyColumns) mkFuncQueryConnectionFld selFltr pLimit hdrs + + funcFldHelper f g pFltr pLimit hdrs = + flip map funcs $ \fi -> + ( f $ FuncQOpCtx (fiName fi) (mkFuncArgItemSeq fi) hdrs colGNameMap pFltr pLimit + , g fi $ fiDescription fi + ) diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index 46420c6573d53..81594a92a8f40 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -1,7 +1,7 @@ module Hasura.GraphQL.Schema ( mkGCtxMap + , mkGCtx , GCtxMap - , getGCtx , GCtx(..) , QueryCtx(..) , MutationCtx(..) @@ -11,6 +11,10 @@ module Hasura.GraphQL.Schema , isAggregateField , qualObjectToName , ppGCtx + , getSelPerm + , isValidObjectName + , mkAdminSelFlds + , noFilter , checkConflictingNode , checkSchemaConflicts @@ -96,18 +100,6 @@ isRelNullable fim ri = isNullable lColInfos = getColInfos lCols allCols isNullable = any pgiIsNullable lColInfos -mkPGColGNameMap :: [PGColumnInfo] -> PGColGNameMap -mkPGColGNameMap cols = Map.fromList $ - flip map cols $ \ci -> (pgiName ci, ci) - -numAggregateOps :: [G.Name] -numAggregateOps = [ "sum", "avg", "stddev", "stddev_samp", "stddev_pop" - , "variance", "var_samp", "var_pop" - ] - -compAggregateOps :: [G.Name] -compAggregateOps = ["max", "min"] - isAggregateField :: G.Name -> Bool isAggregateField = flip elem (numAggregateOps <> compAggregateOps) @@ -225,7 +217,7 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi mkFldMap ty = Map.fromList . concatMap (mkFld ty) mkFld ty = \case SFPGColumn ci -> [((ty, pgiName ci), RFPGColumn ci)] - SFRelationship (RelationshipFieldInfo relInfo allowAgg cols permFilter permLimit maybePkCols _) -> + SFRelationship (RelationshipFieldInfo relInfo allowAgg cols permFilter permLimit _ _) -> let relationshipName = riName relInfo relFld = ( (ty, mkRelName relationshipName) , RFRelationship $ RelationshipField relInfo RFKSimple cols permFilter permLimit @@ -233,14 +225,9 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi aggRelFld = ( (ty, mkAggRelName relationshipName) , RFRelationship $ RelationshipField relInfo RFKAggregate cols permFilter permLimit ) - maybeConnFld = maybePkCols <&> \pkCols -> - ( (ty, mkConnectionRelName relationshipName) - , RFRelationship $ RelationshipField relInfo - (RFKConnection pkCols) cols permFilter permLimit - ) in case riType relInfo of ObjRel -> [relFld] - ArrRel -> bool [relFld] ([relFld, aggRelFld] <> maybe [] pure maybeConnFld) allowAgg + ArrRel -> bool [relFld] [relFld, aggRelFld] allowAgg SFComputedField cf -> pure ( (ty, mkComputedFieldName $ _cfName cf) , RFComputedField cf @@ -261,9 +248,7 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi -- table obj selectObjects = case selPermM of Just (_, selFlds) -> - [ mkTableObj tn descM selFlds - , mkTableEdgeObj tn - , mkTableConnectionObj tn + [ mkTableObj tn False descM selFlds ] Nothing -> [] @@ -350,11 +335,9 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM RootFields { _rootQueryFields = makeFieldMap $ funcQueries - <> funcConnectionQueries <> funcAggQueries <> catMaybes [ getSelDet <$> selM - , getSelConnectionDet <$> selM <*> maybePrimaryKeyColumns , getSelAggDet selM , getPKeySelDet <$> selM <*> primaryKey ] @@ -368,14 +351,11 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM ] } where - maybePrimaryKeyColumns = fmap _pkColumns primaryKey makeFieldMap = mapFromL (_fiName . snd) customRootFields = _tcCustomRootFields tableConfig colGNameMap = mkPGColGNameMap $ getCols fields funcQueries = maybe [] getFuncQueryFlds selM - funcConnectionQueries = fromMaybe [] $ getFuncQueryConnectionFlds - <$> selM <*> maybePrimaryKeyColumns funcAggQueries = maybe [] getFuncAggQueryFlds selM mutHelper :: (ViewInfo -> Bool) -> (a -> b) -> Maybe a -> Maybe b @@ -427,9 +407,6 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM selCustName = getCustomNameWith _tcrfSelect getSelDet (selFltr, pLimit, hdrs, _) = selFldHelper QCSelect (mkSelFld selCustName) selFltr pLimit hdrs - getSelConnectionDet (selFltr, pLimit, hdrs, _) primaryKeyColumns = - selFldHelper (QCSelectConnection primaryKeyColumns) - (mkSelFldConnection Nothing) selFltr pLimit hdrs selAggCustName = getCustomNameWith _tcrfSelectAggregate getSelAggDet (Just (selFltr, pLimit, hdrs, True)) = @@ -452,9 +429,6 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM getFuncQueryFlds (selFltr, pLimit, hdrs, _) = funcFldHelper QCFuncQuery mkFuncQueryFld selFltr pLimit hdrs - getFuncQueryConnectionFlds (selFltr, pLimit, hdrs, _) primaryKeyColumns = - funcFldHelper (QCFuncConnection primaryKeyColumns) mkFuncQueryConnectionFld selFltr pLimit hdrs - getFuncAggQueryFlds (selFltr, pLimit, hdrs, True) = funcFldHelper QCFuncAggQuery mkFuncAggQueryFld selFltr pLimit hdrs getFuncAggQueryFlds _ = [] @@ -465,17 +439,6 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM , g fi $ fiDescription fi ) - mkFuncArgItemSeq functionInfo = - let inputArgs = fiInputArgs functionInfo - in Seq.fromList $ procFuncArgs inputArgs nameFn resultFn - where - nameFn = \case - IAUserProvided fa -> faName fa - IASessionVariables name -> Just name - resultFn arg gName = flip fmap arg $ - \fa -> FunctionArgItem (G.Name gName) (faName fa) (faHasDefault fa) - - getSelPermission :: TableInfo -> RoleName -> Maybe SelPermInfo getSelPermission tabInfo role = Map.lookup role (_tiRolePermInfoMap tabInfo) >>= _permSel @@ -789,11 +752,6 @@ mkGCtxMap annotatedObjects tableCache functionCache actionCache = do pure $ mconcat rootFields -getGCtx :: (CacheRM m) => RoleName -> GCtxMap -> m GCtx -getGCtx rn ctxMap = do - sc <- askSchemaCache - return $ fromMaybe (scDefaultRemoteGCtx sc) $ Map.lookup rn ctxMap - -- pretty print GCtx ppGCtx :: GCtx -> String ppGCtx gCtx = diff --git a/server/src-lib/Hasura/GraphQL/Schema/Common.hs b/server/src-lib/Hasura/GraphQL/Schema/Common.hs index c46f327258256..98071bf89e4e0 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Common.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Common.hs @@ -27,6 +27,11 @@ module Hasura.GraphQL.Schema.Common , mkDescriptionWith , mkFuncArgsTy + + , mkPGColGNameMap + + , numAggregateOps + , compAggregateOps ) where import qualified Data.HashMap.Strict as Map @@ -73,7 +78,8 @@ qualObjectToName :: (ToTxt a) => QualifiedObject a -> G.Name qualObjectToName = G.Name . snakeCaseQualObject addTypeSuffix :: Text -> G.NamedType -> G.NamedType -addTypeSuffix suffix baseType = G.NamedType $ G.unNamedType baseType <> G.Name suffix +addTypeSuffix suffix baseType = + G.NamedType $ G.unNamedType baseType <> G.Name suffix fromInpValL :: [InpValInfo] -> Map.HashMap G.Name InpValInfo fromInpValL = mapFromL _iviName @@ -135,3 +141,15 @@ mkFuncArgsName fn = mkFuncArgsTy :: QualifiedFunction -> G.NamedType mkFuncArgsTy = G.NamedType . mkFuncArgsName + +mkPGColGNameMap :: [PGColumnInfo] -> PGColGNameMap +mkPGColGNameMap cols = Map.fromList $ + flip map cols $ \ci -> (pgiName ci, ci) + +numAggregateOps :: [G.Name] +numAggregateOps = [ "sum", "avg", "stddev", "stddev_samp", "stddev_pop" + , "variance", "var_samp", "var_pop" + ] + +compAggregateOps :: [G.Name] +compAggregateOps = ["max", "min"] diff --git a/server/src-lib/Hasura/GraphQL/Schema/Function.hs b/server/src-lib/Hasura/GraphQL/Schema/Function.hs index 79b4212eb509b..426fe22a08f90 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Function.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Function.hs @@ -5,12 +5,14 @@ module Hasura.GraphQL.Schema.Function , mkFuncQueryConnectionFld , mkFuncAggQueryFld , mkFuncArgsTy + , mkFuncArgItemSeq ) where import qualified Data.Sequence as Seq import qualified Data.Text as T import qualified Language.GraphQL.Draft.Syntax as G +import Hasura.GraphQL.Resolve.Types import Hasura.GraphQL.Schema.Common import Hasura.GraphQL.Schema.Select import Hasura.GraphQL.Validate.Types @@ -135,3 +137,13 @@ mkFuncAggQueryFld funInfo descM = ty = G.toGT $ G.toNT $ mkTableAggTy retTable +mkFuncArgItemSeq :: FunctionInfo -> Seq (InputArgument FunctionArgItem) +mkFuncArgItemSeq functionInfo = + let inputArgs = fiInputArgs functionInfo + in Seq.fromList $ procFuncArgs inputArgs nameFn resultFn + where + nameFn = \case + IAUserProvided fa -> faName fa + IASessionVariables name -> Just name + resultFn arg gName = flip fmap arg $ + \fa -> FunctionArgItem (G.Name gName) (faName fa) (faHasDefault fa) diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index c41d8f7220559..f4a7d63004f51 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -166,11 +166,12 @@ object_relationship: remote_table mkRelationshipField :: Bool -> RelInfo + -> Bool -> Maybe (NonEmpty PGColumnInfo) -> Bool -> [ObjFldInfo] -mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) maybePkCols isNullable = case rTy of - ArrRel -> bool [arrRelFld] ([arrRelFld, aggArrRelFld] <> arrConnectionFld) allowAgg +mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) isRelay maybePkCols isNullable = case rTy of + ArrRel -> bool [arrRelFld] ([arrRelFld, aggArrRelFld] <> connFields) allowAgg ObjRel -> [objRelFld] where objRelFld = mkHsraObjFldInfo (Just "An object relationship") @@ -193,6 +194,8 @@ mkRelationshipField allowAgg (RelInfo rn rTy _ remTab isManual) maybePkCols isNu (mkAggRelName rn) (fromInpValL $ mkSelArgs remTab) $ G.toGT $ G.toNT $ mkTableAggTy remTab + connFields = bool [] arrConnectionFld isRelay + {- type table { col1: colty1 @@ -203,10 +206,11 @@ type table { -} mkTableObj :: QualifiedTable + -> Bool -> Maybe PGDescription -> [SelField] -> ObjTyInfo -mkTableObj tn descM allowedFlds = +mkTableObj tn isRelay descM allowedFlds = mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) TLHasuraType where flds = pgColumnFields <> relFlds <> computedFlds @@ -214,7 +218,7 @@ mkTableObj tn descM allowedFlds = relFlds = concatMap mkRelationshipField' $ getRelationshipFields allowedFlds computedFlds = map mkComputedFieldFld $ getComputedFields allowedFlds mkRelationshipField' (RelationshipFieldInfo relInfo allowAgg _ _ _ maybePkCols isNullable) = - mkRelationshipField allowAgg relInfo maybePkCols isNullable + mkRelationshipField allowAgg relInfo isRelay maybePkCols isNullable desc = mkDescriptionWith descM $ "columns and relationships of " <>> tn {- @@ -255,7 +259,7 @@ mkTableAggregateFieldsObj -> ([PGColumnInfo], [G.Name]) -> ([PGColumnInfo], [G.Name]) -> ObjTyInfo -mkTableAggregateFieldsObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) = +mkTableAggregateFieldsObj tn (numCols, numericAggregateOps) (compCols, compareAggregateOps) = mkHsraObjTyInfo (Just desc) (mkTableAggregateFieldsTy tn) Set.empty $ mapFromL _fiName $ countFld : (numFlds <> compFlds) where @@ -272,8 +276,8 @@ mkTableAggregateFieldsObj tn (numCols, numAggregateOps) (compCols, compAggregate distinctInpVal = InpValInfo Nothing "distinct" Nothing $ G.toGT $ mkScalarTy PGBoolean - numFlds = bool (map mkColumnOpFld numAggregateOps) [] $ null numCols - compFlds = bool (map mkColumnOpFld compAggregateOps) [] $ null compCols + numFlds = bool (map mkColumnOpFld numericAggregateOps) [] $ null numCols + compFlds = bool (map mkColumnOpFld compareAggregateOps) [] $ null compCols mkColumnOpFld op = mkHsraObjFldInfo Nothing op Map.empty $ G.toGT $ mkTableColAggregateFieldsTy op tn @@ -351,7 +355,7 @@ mkTableConnectionObj tn = where desc = G.Description $ "A Relay Connection object on " <>> tn pageInfoFld = mkHsraObjFldInfo Nothing "pageInfo" Map.empty $ - G.toGT $ G.toNT $ pageInfoTy + G.toGT $ G.toNT pageInfoTy edgesFld = mkHsraObjFldInfo Nothing "edges" Map.empty $ G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableEdgeTy tn diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index 3ee15ccba6a13..3180ddd3cafad 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -31,15 +31,16 @@ runGQ => RequestId -> UserInfo -> [N.Header] + -> E.GraphQLAPIType -> GQLReq GQLQueryText -> m (HttpResponse EncJSON) -runGQ reqId userInfo reqHdrs req = do +runGQ reqId userInfo reqHdrs apiType req = do -- The response and misc telemetry data: let telemTransport = Telem.HTTP (telemTimeTot_DT, (telemCacheHit, telemLocality, (telemTimeIO_DT, telemQueryType, !resp))) <- withElapsedTime $ do E.ExecutionCtx _ sqlGenCtx pgExecCtx planCache sc scVer httpManager enableAL <- ask (telemCacheHit, execPlan) <- E.getResolvedExecPlan pgExecCtx planCache - userInfo sqlGenCtx enableAL sc scVer httpManager reqHdrs req + userInfo sqlGenCtx enableAL sc scVer apiType httpManager reqHdrs req case execPlan of E.GExPHasura resolvedOp -> do (telemTimeIO, telemQueryType, respHdrs, resp) <- runHasuraGQ reqId req userInfo resolvedOp @@ -63,12 +64,13 @@ runGQBatched => RequestId -> UserInfo -> [N.Header] + -> E.GraphQLAPIType -> GQLBatchedReqs GQLQueryText -> m (HttpResponse EncJSON) -runGQBatched reqId userInfo reqHdrs reqs = +runGQBatched reqId userInfo reqHdrs apiType reqs = case reqs of GQLSingleRequest req -> - runGQ reqId userInfo reqHdrs req + runGQ reqId userInfo reqHdrs apiType req GQLBatchedReqs batch -> do -- It's unclear what we should do if we receive multiple -- responses with distinct headers, so just do the simplest thing @@ -79,7 +81,7 @@ runGQBatched reqId userInfo reqHdrs reqs = . map (either (encJFromJValue . encodeGQErr False) _hrBody) try = flip catchError (pure . Left) . fmap Right fmap removeHeaders $ - traverse (try . runGQ reqId userInfo reqHdrs) batch + traverse (try . runGQ reqId userInfo reqHdrs apiType) batch runHasuraGQ :: ( MonadIO m diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index 3102cd6cc8ebf..8ca2d6edf3385 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -94,6 +94,7 @@ data WSConnData -- are not tracked here , _wscOpMap :: !OperationMap , _wscErrRespTy :: !ErrRespType + , _wscAPIType :: !E.GraphQLAPIType } type WSServer = WS.WSServer WSConnData @@ -203,11 +204,11 @@ onConn :: (MonadIO m) => L.Logger L.Hasura -> CorsPolicy -> WS.OnConnH m WSConnData onConn (L.Logger logger) corsPolicy wsId requestHead = do res <- runExceptT $ do - errType <- checkPath + (errType, apiType) <- checkPath let reqHdrs = WS.requestHeaders requestHead headers <- maybe (return reqHdrs) (flip enforceCors reqHdrs . snd) getOrigin - return (WsHeaders $ filterWsHeaders headers, errType) - either reject (uncurry accept) res + return (WsHeaders $ filterWsHeaders headers, errType, apiType) + either reject accept res where keepAliveAction wsConn = liftIO $ forever $ do @@ -225,12 +226,13 @@ onConn (L.Logger logger) corsPolicy wsId requestHead = do currTime <- TC.getCurrentTime sleep $ fromUnits $ TC.diffUTCTime expTime currTime - accept hdrs errType = do + accept (hdrs, errType, apiType) = do logger $ mkWsInfoLog Nothing (WsConnInfo wsId Nothing Nothing) EAccepted connData <- liftIO $ WSConnData <$> STM.newTVarIO (CSNotInitialised hdrs) <*> STMMap.newIO <*> pure errType + <*> pure apiType let acceptRequest = WS.defaultAcceptRequest { WS.acceptSubprotocol = Just "graphql-ws"} return $ Right $ WS.AcceptWith connData acceptRequest keepAliveAction tokenExpiryHandler @@ -243,8 +245,9 @@ onConn (L.Logger logger) corsPolicy wsId requestHead = do (BL.toStrict $ J.encode $ encodeGQLErr False qErr) checkPath = case WS.requestPath requestHead of - "/v1alpha1/graphql" -> return ERTLegacy - "/v1/graphql" -> return ERTGraphqlCompliant + "/v1alpha1/graphql" -> return (ERTLegacy, E.GATGeneral) + "/v1/graphql" -> return (ERTGraphqlCompliant, E.GATGeneral) + "/v1/relay" -> return (ERTGraphqlCompliant, E.GATRelay) _ -> throw404 "only '/v1/graphql', '/v1alpha1/graphql' are supported on websockets" @@ -306,7 +309,7 @@ onStart serverEnv wsConn (StartMsg opId q) = catchAndIgnore $ do requestId <- getRequestId reqHdrs (sc, scVer) <- liftIO getSchemaCache execPlanE <- runExceptT $ E.getResolvedExecPlan pgExecCtx - planCache userInfo sqlGenCtx enableAL sc scVer httpMgr reqHdrs q + planCache userInfo sqlGenCtx enableAL sc scVer apiType httpMgr reqHdrs q (telemCacheHit, execPlan) <- either (withComplete . preExecErr requestId) return execPlanE let execCtx = E.ExecutionCtx logger sqlGenCtx pgExecCtx @@ -397,7 +400,7 @@ onStart serverEnv wsConn (StartMsg opId q) = catchAndIgnore $ do WSServerEnv logger pgExecCtx lqMap getSchemaCache httpMgr _ sqlGenCtx planCache _ enableAL = serverEnv - WSConnData userInfoR opMap errRespTy = WS.getData wsConn + WSConnData userInfoR opMap errRespTy apiType = WS.getData wsConn logOpEv opTy reqId = logWSEvent logger wsConn $ EOperation opDet @@ -522,7 +525,7 @@ logWSEvent (L.Logger logger) wsConn wsEv = do _ -> (Nothing, Nothing) liftIO $ logger $ WSLog logLevel $ WSLogInfo userVarsM (WsConnInfo wsId tokenExpM Nothing) wsEv where - WSConnData userInfoR _ _ = WS.getData wsConn + WSConnData userInfoR _ _ _ = WS.getData wsConn wsId = WS.getWSId wsConn logLevel = bool L.LevelInfo L.LevelError isError isError = case wsEv of diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs index ee37fafc15cde..ceb475b2f1cae 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs @@ -32,6 +32,7 @@ import Data.Aeson import Data.List (nub) import qualified Hasura.GraphQL.Context as GC +import qualified Hasura.GraphQL.RelaySchema as Relay import qualified Hasura.GraphQL.Schema as GS import qualified Hasura.GraphQL.Validate.Types as VT import qualified Hasura.Incremental as Inc @@ -176,6 +177,9 @@ buildSchemaCacheRule = proc (catalogMetadata, invalidationKeys) -> do , _boActions resolvedOutputs ) + -- Step 4: Build the relay GraphQL schema + relayGQLSchema <- bindA -< Relay.mkRelayGCtxMap (_boTables resolvedOutputs) (_boFunctions resolvedOutputs) + returnA -< SchemaCache { scTables = _boTables resolvedOutputs , scActions = _boActions resolvedOutputs @@ -185,6 +189,7 @@ buildSchemaCacheRule = proc (catalogMetadata, invalidationKeys) -> do , scCustomTypes = _boCustomTypes resolvedOutputs , scGCtxMap = gqlSchema , scDefaultRemoteGCtx = remoteGQLSchema + , scRelayGCtxMap = relayGQLSchema , scDepMap = resolvedDependencies , scInconsistentObjs = inconsistentObjects <> dependencyInconsistentObjects <> toList gqlSchemaInconsistentObjects diff --git a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs index 9955a8e2ed69f..8e41ad6f50c38 100644 --- a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs +++ b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs @@ -198,6 +198,7 @@ data SchemaCache , scCustomTypes :: !(NonObjectTypeMap, AnnotatedObjects) , scGCtxMap :: !GC.GCtxMap , scDefaultRemoteGCtx :: !GC.GCtx + , scRelayGCtxMap :: !GC.GCtxMap , scDepMap :: !DepMap , scInconsistentObjs :: ![InconsistentMetadata] } deriving (Show, Eq) diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index fbaa8d953f000..51c8bd7ea9148 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -313,8 +313,10 @@ v1QueryHandler query = do instanceId <- scInstanceId . hcServerCtx <$> ask runQuery pgExecCtx instanceId userInfo schemaCache httpMgr sqlGenCtx (SystemDefined False) query -v1Alpha1GQHandler :: (HasVersion, MonadIO m) => GH.GQLBatchedReqs GH.GQLQueryText -> Handler m (HttpResponse EncJSON) -v1Alpha1GQHandler query = do +v1Alpha1GQHandler + :: (HasVersion, MonadIO m) + => E.GraphQLAPIType -> GH.GQLBatchedReqs GH.GQLQueryText -> Handler m (HttpResponse EncJSON) +v1Alpha1GQHandler apiType query = do userInfo <- asks hcUser reqHeaders <- asks hcReqHeaders manager <- scManager . hcServerCtx <$> ask @@ -328,13 +330,19 @@ v1Alpha1GQHandler query = do requestId <- asks hcRequestId let execCtx = E.ExecutionCtx logger sqlGenCtx pgExecCtx planCache (lastBuiltSchemaCache sc) scVer manager enableAL - flip runReaderT execCtx $ GH.runGQBatched requestId userInfo reqHeaders query + flip runReaderT execCtx $ GH.runGQBatched requestId userInfo reqHeaders apiType query v1GQHandler :: (HasVersion, MonadIO m) => GH.GQLBatchedReqs GH.GQLQueryText -> Handler m (HttpResponse EncJSON) -v1GQHandler = v1Alpha1GQHandler +v1GQHandler = v1Alpha1GQHandler E.GATGeneral + +v1GQRelayHandler + :: (HasVersion, MonadIO m) + => GH.GQLBatchedReqs GH.GQLQueryText + -> Handler m (HttpResponse EncJSON) +v1GQRelayHandler = v1Alpha1GQHandler E.GATRelay gqlExplainHandler :: (MonadIO m) => GE.GQLExplain -> Handler m (HttpResponse EncJSON) gqlExplainHandler query = do @@ -601,11 +609,14 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do when enableGraphQL $ do Spock.post "v1alpha1/graphql" $ spockAction GH.encodeGQErr id $ - mkPostHandler $ mkAPIRespHandler v1Alpha1GQHandler + mkPostHandler $ mkAPIRespHandler $ v1Alpha1GQHandler E.GATGeneral Spock.post "v1/graphql" $ spockAction GH.encodeGQErr allMod200 $ mkPostHandler $ mkAPIRespHandler v1GQHandler + Spock.post "v1/relay" $ spockAction GH.encodeGQErr allMod200 $ + mkPostHandler $ mkAPIRespHandler v1GQRelayHandler + when (isDeveloperAPIEnabled serverCtx) $ do Spock.get "dev/ekg" $ spockAction encodeQErr id $ mkGetHandler $ do From e797e32e0a141fdce0eb913987bdc7db9c0d3eae Mon Sep 17 00:00:00 2001 From: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Date: Tue, 26 May 2020 17:14:23 +0530 Subject: [PATCH 10/29] implement 'Node' interface and top level 'node' field resolver --- server/src-lib/Hasura/GraphQL/Execute.hs | 39 ++------ .../Hasura/GraphQL/Execute/LiveQuery/Plan.hs | 35 +++++--- server/src-lib/Hasura/GraphQL/Explain.hs | 6 +- server/src-lib/Hasura/GraphQL/NormalForm.hs | 59 +++++++------ server/src-lib/Hasura/GraphQL/RelaySchema.hs | 88 ++++++++++++++++--- server/src-lib/Hasura/GraphQL/Resolve.hs | 20 +++-- .../src-lib/Hasura/GraphQL/Resolve/BoolExp.hs | 2 + .../src-lib/Hasura/GraphQL/Resolve/Context.hs | 13 +-- .../Hasura/GraphQL/Resolve/Introspect.hs | 4 +- .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 67 ++++++++++++-- .../src-lib/Hasura/GraphQL/Resolve/Types.hs | 16 +++- server/src-lib/Hasura/GraphQL/Schema.hs | 2 +- .../src-lib/Hasura/GraphQL/Schema/Common.hs | 11 +++ .../src-lib/Hasura/GraphQL/Schema/OrderBy.hs | 12 +-- .../src-lib/Hasura/GraphQL/Schema/Select.hs | 26 +++++- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 20 ++--- server/src-lib/Hasura/GraphQL/Validate.hs | 11 ++- .../Hasura/GraphQL/Validate/SelectionSet.hs | 67 +++++++------- .../src-lib/Hasura/GraphQL/Validate/Types.hs | 4 +- server/src-lib/Hasura/Prelude.hs | 9 ++ server/src-lib/Hasura/RQL/DML/Select.hs | 12 +-- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 47 +++++++--- server/src-lib/Hasura/RQL/DML/Select/Types.hs | 2 + server/src-lib/Hasura/Server/Utils.hs | 2 - .../basic/select_query_fragment_cycles.yaml | 2 +- server/tests-py/test_schema_stitching.py | 4 +- 26 files changed, 388 insertions(+), 192 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index a2738cd6e5d81..8e1bdb9e1644f 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -236,9 +236,9 @@ getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx (allowQueryActionExecuter httpManager reqHeaders) selSet traverse_ (addPlanToCache . EP.RPQuery) plan return $ ExOpQuery queryTx (Just genSql) - VQ.RSubscription alias fld -> do + VQ.RSubscription fields -> do (lqOp, plan) <- getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability - (restrictActionExecuter "query actions cannot be run as a subscription") alias fld + (restrictActionExecuter "query actions cannot be run as a subscription") fields traverse_ (addPlanToCache . EP.RPSubs) plan return $ ExOpSubs lqOp @@ -347,33 +347,6 @@ getMutOp ctx sqlGenCtx userInfo manager reqHeaders selSet = ordByCtx = _gOrdByCtx ctx insCtxMap = _gInsCtxMap ctx -getSubsOpM - :: ( MonadError QErr m - , MonadReader r m - , Has QueryCtxMap r - , Has FieldMap r - , Has OrdByCtx r - , Has SQLGenCtx r - , Has UserInfo r - , MonadIO m - , HasVersion - ) - => PGExecCtx - -> QueryReusability - -> G.Alias - -> VQ.Field - -> QueryActionExecuter - -> m (EL.LiveQueryPlan, Maybe EL.ReusableLiveQueryPlan) -getSubsOpM pgExecCtx initialReusability alias fld actionExecuter = - case VQ._fName fld of - "__typename" -> - throwVE "you cannot create a subscription on '__typename' field" - _ -> do - (astUnresolved, finalReusability) <- runReusabilityTWith initialReusability $ - GR.queryFldToPGAST fld actionExecuter - let varTypes = finalReusability ^? _Reusable - EL.buildLiveQueryPlan pgExecCtx alias astUnresolved varTypes - getSubsOp :: ( MonadError QErr m , MonadIO m @@ -385,11 +358,11 @@ getSubsOp -> UserInfo -> QueryReusability -> QueryActionExecuter - -> G.Alias - -> VQ.Field + -> VQ.ObjectSelectionSet -> m (EL.LiveQueryPlan, Maybe EL.ReusableLiveQueryPlan) -getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability actionExecuter alias fld = - runE gCtx sqlGenCtx userInfo $ getSubsOpM pgExecCtx queryReusability alias fld actionExecuter +getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability actionExecuter = + runE gCtx sqlGenCtx userInfo . + EL.buildLiveQueryPlan pgExecCtx queryReusability actionExecuter execRemoteGQ :: ( HasVersion diff --git a/server/src-lib/Hasura/GraphQL/Execute/LiveQuery/Plan.hs b/server/src-lib/Hasura/GraphQL/Execute/LiveQuery/Plan.hs index 2ea103244913a..54f8f16cd1dc7 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/LiveQuery/Plan.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/LiveQuery/Plan.hs @@ -30,6 +30,7 @@ import qualified Data.Aeson.Casing as J import qualified Data.Aeson.Extended as J import qualified Data.Aeson.TH as J import qualified Data.HashMap.Strict as Map +import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.Text as T import qualified Data.UUID.V4 as UUID import qualified Database.PG.Query as Q @@ -50,6 +51,10 @@ import qualified Hasura.SQL.DML as S import Hasura.Db import Hasura.EncJSON +import Hasura.GraphQL.Resolve.Action +import Hasura.GraphQL.Resolve.Types +import Hasura.GraphQL.Validate.SelectionSet +import Hasura.GraphQL.Validate.Types import Hasura.RQL.Types import Hasura.Server.Version (HasVersion) import Hasura.SQL.Error @@ -62,7 +67,7 @@ import Hasura.SQL.Value newtype MultiplexedQuery = MultiplexedQuery { unMultiplexedQuery :: Q.Query } deriving (Show, Eq, Hashable, J.ToJSON) -mkMultiplexedQuery :: Map.HashMap G.Alias GR.QueryRootFldResolved -> MultiplexedQuery +mkMultiplexedQuery :: OMap.InsOrdHashMap G.Alias GR.QueryRootFldResolved -> MultiplexedQuery mkMultiplexedQuery rootFields = MultiplexedQuery . Q.fromBuilder . toSQL $ S.mkSelect { S.selExtr = -- SELECT _subs.result_id, _fld_resp.root AS result @@ -83,13 +88,13 @@ mkMultiplexedQuery rootFields = MultiplexedQuery . Q.fromBuilder . toSQL $ S.mkS selectRootFields = S.mkSelect { S.selExtr = [S.Extractor rootFieldsJsonAggregate (Just . S.Alias $ Iden "root")] , S.selFrom = Just . S.FromExp $ - flip map (Map.toList rootFields) $ \(fieldAlias, resolvedAST) -> + flip map (OMap.toList rootFields) $ \(fieldAlias, resolvedAST) -> GR.toSQLFromItem (S.Alias $ aliasToIden fieldAlias) resolvedAST } -- json_build_object('field1', field1.root, 'field2', field2.root, ...) rootFieldsJsonAggregate = S.SEFnApp "json_build_object" rootFieldsJsonPairs Nothing - rootFieldsJsonPairs = flip concatMap (Map.keys rootFields) $ \fieldAlias -> + rootFieldsJsonPairs = flip concatMap (OMap.keys rootFields) $ \fieldAlias -> [ S.SELit (G.unName $ G.unAlias fieldAlias) , mkQualIden (aliasToIden fieldAlias) (Iden "root") ] @@ -264,19 +269,28 @@ buildLiveQueryPlan :: ( MonadError QErr m , MonadReader r m , Has UserInfo r + , Has FieldMap r + , Has OrdByCtx r + , Has QueryCtxMap r + , Has SQLGenCtx r , MonadIO m + , HasVersion ) => PGExecCtx - -> G.Alias - -> GR.QueryRootFldUnresolved - -> Maybe GV.ReusableVariableTypes + -> QueryReusability + -> QueryActionExecuter + -> ObjectSelectionSet -> m (LiveQueryPlan, Maybe ReusableLiveQueryPlan) -buildLiveQueryPlan pgExecCtx alias unresolvedAST varTypes = do - (resolvedAST, (queryVariableValues, syntheticVariableValues)) <- - flip runStateT mempty $ GR.traverseQueryRootFldAST resolveMultiplexedValue unresolvedAST +buildLiveQueryPlan pgExecCtx initialReusability actionExecuter selectionSet = do + ((resolvedASTMap, (queryVariableValues, syntheticVariableValues)), finalReusability) <- + runReusabilityTWith initialReusability $ + flip runStateT mempty $ flip OMap.traverseWithKey (unAliasedFields $ unObjectSelectionSet selectionSet) $ + \_ field -> do + unresolvedAST <- GR.queryFldToPGAST field actionExecuter + GR.traverseQueryRootFldAST resolveMultiplexedValue unresolvedAST userInfo <- asks getter - let multiplexedQuery = mkMultiplexedQuery $ Map.singleton alias resolvedAST + let multiplexedQuery = mkMultiplexedQuery resolvedASTMap roleName = _uiRole userInfo parameterizedPlan = ParameterizedLiveQueryPlan roleName multiplexedQuery @@ -287,6 +301,7 @@ buildLiveQueryPlan pgExecCtx alias unresolvedAST varTypes = do validatedSyntheticVars <- validateVariables pgExecCtx (toList syntheticVariableValues) let cohortVariables = CohortVariables (_uiSession userInfo) validatedQueryVars validatedSyntheticVars plan = LiveQueryPlan parameterizedPlan cohortVariables + varTypes = finalReusability ^? _Reusable reusablePlan = ReusableLiveQueryPlan parameterizedPlan validatedSyntheticVars <$> varTypes pure (plan, reusablePlan) diff --git a/server/src-lib/Hasura/GraphQL/Explain.hs b/server/src-lib/Hasura/GraphQL/Explain.hs index fb79021339ee2..81508ce94ca19 100644 --- a/server/src-lib/Hasura/GraphQL/Explain.hs +++ b/server/src-lib/Hasura/GraphQL/Explain.hs @@ -131,16 +131,16 @@ explainGQLQuery pgExecCtx sc sqlGenCtx enableAL actionExecuter (GQLExplain query (gCtx, rootSelSet) <- case execPlan of E.GExPHasura (gCtx, rootSelSet) -> return (gCtx, rootSelSet) - E.GExPRemote _ _ _ -> + E.GExPRemote{} -> throw400 InvalidParams "only hasura queries can be explained" case rootSelSet of GV.RQuery selSet -> runInTx $ encJFromJValue <$> GV.traverseObjectSelectionSet selSet (explainField userInfo gCtx sqlGenCtx actionExecuter) GV.RMutation _ -> throw400 InvalidParams "only queries can be explained" - GV.RSubscription alias rootField -> do + GV.RSubscription fields -> do (plan, _) <- E.getSubsOp pgExecCtx gCtx sqlGenCtx userInfo - queryReusability actionExecuter alias rootField + queryReusability actionExecuter fields runInTx $ encJFromJValue <$> E.explainLiveQueryPlan plan where apiType = bool E.GATGeneral E.GATRelay $ fromMaybe False maybeIsRelay diff --git a/server/src-lib/Hasura/GraphQL/NormalForm.hs b/server/src-lib/Hasura/GraphQL/NormalForm.hs index 3436b2d2fd4e6..58e20980168ad 100644 --- a/server/src-lib/Hasura/GraphQL/NormalForm.hs +++ b/server/src-lib/Hasura/GraphQL/NormalForm.hs @@ -1,5 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilyDependencies #-} -{-# LANGUAGE RecordWildCards #-} module Hasura.GraphQL.NormalForm ( Selection(..) , NormalizedSelection @@ -7,7 +7,7 @@ module Hasura.GraphQL.NormalForm , NormalizedField , SelectionSet(..) , RootSelectionSet(..) - , toGraphQLOperation + -- , toGraphQLOperation , ArgsMap , Field(..) , Typename(..) @@ -19,6 +19,7 @@ module Hasura.GraphQL.NormalForm , ObjectSelectionSetMap , traverseObjectSelectionSet , InterfaceSelectionSet + , asInterfaceSelectionSet , getMemberSelectionSet , UnionSelectionSet , ScopedSelectionSet(..) @@ -47,10 +48,10 @@ import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict.InsOrd.Extended as OMap import qualified Language.GraphQL.Draft.Syntax as G +import qualified Hasura.RQL.Types.Column as RQL +import qualified Hasura.RQL.Types.Error as RQL import Hasura.SQL.Types import Hasura.SQL.Value -import qualified Hasura.RQL.Types.Column as RQL -import qualified Hasura.RQL.Types.Error as RQL data Selection f s = SelectionField !G.Alias !f @@ -109,31 +110,29 @@ type UnionSelectionSet = ScopedSelectionSet Typename data RootSelectionSet = RQuery !ObjectSelectionSet | RMutation !ObjectSelectionSet - | RSubscription !G.Alias !Field + | RSubscription !ObjectSelectionSet deriving (Show, Eq) -toGraphQLOperation :: RootSelectionSet -> G.ExecutableDefinition -toGraphQLOperation = \case - RQuery selectionSet -> - mkExecutableDefinition G.OperationTypeQuery $ - toGraphQLSelectionSet $ SelectionSetObject selectionSet - RMutation selectionSet -> - mkExecutableDefinition G.OperationTypeQuery $ - toGraphQLSelectionSet $ SelectionSetObject selectionSet - RSubscription alias field -> - mkExecutableDefinition G.OperationTypeSubscription $ - toGraphQLSelectionSet $ SelectionSetObject $ ObjectSelectionSet $ - AliasedFields $ OMap.singleton alias field - where - mkExecutableDefinition operationType selectionSet = - G.ExecutableDefinitionOperation $ G.OperationDefinitionTyped $ - G.TypedOperationDefinition - { G._todName = Nothing -- TODO, store the name too? - , G._todDirectives = [] - , G._todType = operationType - , G._todVariableDefinitions = [] - , G._todSelectionSet = selectionSet - } +-- toGraphQLOperation :: RootSelectionSet -> G.ExecutableDefinition +-- toGraphQLOperation = \case +-- RQuery selectionSet -> +-- mkExecutableDefinition G.OperationTypeQuery $ +-- toGraphQLSelectionSet $ SelectionSetObject selectionSet +-- RMutation selectionSet -> +-- mkExecutableDefinition G.OperationTypeQuery $ +-- toGraphQLSelectionSet $ SelectionSetObject selectionSet +-- RSubscription opDef _ -> +-- G.ExecutableDefinitionOperation $ G.OperationDefinitionTyped opDef +-- where +-- mkExecutableDefinition operationType selectionSet = +-- G.ExecutableDefinitionOperation $ G.OperationDefinitionTyped $ +-- G.TypedOperationDefinition +-- { G._todName = Nothing -- TODO, store the name too? +-- , G._todDirectives = [] +-- , G._todType = operationType +-- , G._todVariableDefinitions = [] +-- , G._todSelectionSet = selectionSet +-- } data SelectionSet @@ -165,6 +164,12 @@ getInterfaceSelectionSet = \case SelectionSetInterface s -> pure s _ -> Nothing +asInterfaceSelectionSet + :: (MonadError RQL.QErr m) => SelectionSet -> m InterfaceSelectionSet +asInterfaceSelectionSet selectionSet = + onNothing (getInterfaceSelectionSet selectionSet) $ + RQL.throw500 "expecting InterfaceSelectionSet" + type ArgsMap = Map.HashMap G.Name AnnInpVal data Field diff --git a/server/src-lib/Hasura/GraphQL/RelaySchema.hs b/server/src-lib/Hasura/GraphQL/RelaySchema.hs index 01ecebfa14609..ac17acf23aaab 100644 --- a/server/src-lib/Hasura/GraphQL/RelaySchema.hs +++ b/server/src-lib/Hasura/GraphQL/RelaySchema.hs @@ -4,8 +4,8 @@ import Control.Lens.Extended hiding (op) import qualified Data.HashMap.Strict as Map import qualified Data.HashSet as Set - import qualified Data.Text as T +import qualified Language.GraphQL.Draft.Syntax as G import Hasura.GraphQL.Context import Hasura.GraphQL.Resolve.Types @@ -24,31 +24,49 @@ import Hasura.GraphQL.Schema.Function import Hasura.GraphQL.Schema.OrderBy import Hasura.GraphQL.Schema.Select +mkNodeInterface :: [QualifiedTable] -> IFaceTyInfo +mkNodeInterface relayTableNames = + let description = G.Description "An object with globally unique ID" + in IFaceTyInfo (Just description) nodeType (mapFromL _fiName [idField]) $ + Set.fromList $ map mkTableTy relayTableNames + where + idField = + let description = G.Description "A globally unique identifier" + in mkHsraObjFldInfo (Just description) "id" mempty nodeIdType + mkRelayGCtxMap :: forall m. (MonadError QErr m) => TableCache -> FunctionCache -> m GCtxMap mkRelayGCtxMap tableCache functionCache = do - typesMapL <- mapM (mkRelayGCtxMapTable tableCache functionCache) $ - filter (tableFltr . _tiCoreInfo) $ Map.elems tableCache + typesMapL <- mapM (mkRelayGCtxMapTable tableCache functionCache) relayTables typesMap <- combineTypes typesMapL let gCtxMap = flip Map.map typesMap $ \(ty, flds) -> mkGCtx ty flds mempty pure $ Map.map (flip RoleContext Nothing) gCtxMap where - tableFltr ti = not (isSystemDefined $ _tciSystemDefined ti) && isValidObjectName (_tciName ti) + relayTables = + filter (tableFltr . _tiCoreInfo) $ Map.elems tableCache + + tableFltr ti = + not (isSystemDefined $ _tciSystemDefined ti) + && isValidObjectName (_tciName ti) + && isJust (_tciPrimaryKey ti) combineTypes :: [Map.HashMap RoleName (TyAgg, RootFields)] -> m (Map.HashMap RoleName (TyAgg, RootFields)) combineTypes maps = do let listMap = foldr (Map.unionWith (++) . Map.map pure) mempty maps - flip Map.traverseWithKey listMap $ \_ typeList -> do - let tyAgg = mconcat $ map (^. _1) typeList - rootFields <- combineRootFields $ map (^. _2) typeList + flip Map.traverseWithKey listMap $ \roleName typeList -> do + let relayTableNames = map (_tciName . _tiCoreInfo) relayTables + tyAgg = addTypeInfoToTyAgg + (TIIFace $ mkNodeInterface relayTableNames) $ + mconcat $ map (^. _1) typeList + rootFields <- combineRootFields roleName $ map (^. _2) typeList pure (tyAgg, rootFields) - combineRootFields :: [RootFields] -> m RootFields - combineRootFields rootFields = do + combineRootFields :: RoleName -> [RootFields] -> m RootFields + combineRootFields roleName rootFields = do let duplicateQueryFields = duplicates $ concatMap (Map.keys . _rootQueryFields) rootFields duplicateMutationFields = duplicates $ @@ -63,7 +81,7 @@ mkRelayGCtxMap tableCache functionCache = do throw400 Unexpected $ "following mutation root fields are duplicated: " <> showNames duplicateMutationFields - pure $ mconcat rootFields + pure $ mconcat $ mkNodeQueryRootFields roleName relayTables : rootFields mkRelayGCtxMapTable :: (MonadError QErr m) @@ -181,7 +199,8 @@ mkRelayGCtxRole' tn descM selPermM pkeyCols funcs = -- table obj selectObjects = case selPermM of Just (_, selFlds) -> - [ mkTableObj tn True descM selFlds + [ (mkRelayTableObj tn descM selFlds) + {_otiImplIFaces = Set.singleton nodeType} , mkTableEdgeObj tn , mkTableConnectionObj tn ] @@ -217,7 +236,10 @@ mkRelayGCtxRole' tn descM selPermM pkeyCols funcs = compFldsObjs = bool (map mkCompObjFld compAggregateOps) [] $ null compCols in numFldsObjs <> compFldsObjs -- the fields used in table object - selObjFldsM = mkFldMap (mkTableTy tn) <$> selFldsM + nodeFieldM = RFNodeId tn . _pkColumns <$> pkeyCols + selObjFldsM = mkFldMap (mkTableTy tn) <$> selFldsM >>= + \fm -> nodeFieldM <&> \nodeField -> + Map.insert (mkTableTy tn, "id") nodeField fm -- the scalar set for table_by_pk arguments selByPkScalarSet = pkeyCols ^.. folded.to _pkColumns.folded.to pgiType._PGColumnScalar @@ -238,6 +260,16 @@ mkRelayGCtxRole' tn descM selPermM pkeyCols funcs = computedFieldFuncArgsInps = map (TIInpObj . fst) computedFieldReqTypes computedFieldFuncArgScalars = Set.fromList $ concatMap snd computedFieldReqTypes +mkSelectOpCtx + :: QualifiedTable + -> [PGColumnInfo] + -> (AnnBoolExpPartialSQL, Maybe Int, [T.Text]) -- select filter + -> SelOpCtx +mkSelectOpCtx tn allCols (fltr, pLimit, hdrs) = + SelOpCtx tn hdrs colGNameMap fltr pLimit + where + colGNameMap = mkPGColGNameMap allCols + getRelayRootFldsRole' :: QualifiedTable -> Maybe (PrimaryKey PGColumnInfo) @@ -267,7 +299,7 @@ getRelayRootFldsRole' tn primaryKey fields funcs selM = (mkSelFldConnection Nothing) selFltr pLimit hdrs selFldHelper f g pFltr pLimit hdrs = - ( f $ SelOpCtx tn hdrs colGNameMap pFltr pLimit + ( f $ mkSelectOpCtx tn (getCols fields) (pFltr, pLimit, hdrs) , g tn ) @@ -279,3 +311,33 @@ getRelayRootFldsRole' tn primaryKey fields funcs selM = ( f $ FuncQOpCtx (fiName fi) (mkFuncArgItemSeq fi) hdrs colGNameMap pFltr pLimit , g fi $ fiDescription fi ) + +mkNodeQueryRootFields :: RoleName -> [TableInfo] -> RootFields +mkNodeQueryRootFields roleName relayTables = + RootFields (mapFromL (_fiName . snd) [nodeQueryDet]) mempty + where + nodeQueryDet = + ( QCNodeSelect nodeSelMap + , nodeQueryField + ) + + nodeQueryField = + let nodeParams = fromInpValL $ pure $ + InpValInfo (Just $ G.Description "A globally unique id") + "id" Nothing nodeIdType + in mkHsraObjFldInfo Nothing "node" nodeParams $ G.toGT nodeType + + nodeSelMap = + Map.fromList $ flip mapMaybe relayTables $ \table -> + let tableName = _tciName $ _tiCoreInfo table + allColumns = getCols $ _tciFieldInfoMap $ _tiCoreInfo table + selectPermM = _permSel <$> Map.lookup roleName + (_tiRolePermInfoMap table) + permDetailsM = join selectPermM <&> \perm -> + ( spiFilter perm + , spiLimit perm + , spiRequiredHeaders perm + ) + adminPermDetails = (noFilter, Nothing, []) + in (mkTableTy tableName,) . mkSelectOpCtx tableName allColumns + <$> bool permDetailsM (Just adminPermDetails) (isAdmin roleName) diff --git a/server/src-lib/Hasura/GraphQL/Resolve.hs b/server/src-lib/Hasura/GraphQL/Resolve.hs index 2efe8d5fc25a1..ac6132b6f487d 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve.hs @@ -38,12 +38,14 @@ import qualified Hasura.GraphQL.Resolve.Insert as RI import qualified Hasura.GraphQL.Resolve.Introspect as RIntro import qualified Hasura.GraphQL.Resolve.Mutation as RM import qualified Hasura.GraphQL.Resolve.Select as RS +import qualified Hasura.GraphQL.Schema.Common as GS import qualified Hasura.GraphQL.Validate as V import qualified Hasura.RQL.DML.Select as DS import qualified Hasura.SQL.DML as S data QueryRootFldAST v - = QRFPk !(DS.AnnSimpleSelG v) + = QRFNode !(DS.AnnSimpleSelG v) + | QRFPk !(DS.AnnSimpleSelG v) | QRFSimple !(DS.AnnSimpleSelG v) | QRFAgg !(DS.AnnAggregateSelectG v) | QRFConnection !(DS.ConnectionSelect v) @@ -61,6 +63,7 @@ traverseQueryRootFldAST -> QueryRootFldAST a -> f (QueryRootFldAST b) traverseQueryRootFldAST f = \case + QRFNode s -> QRFNode <$> DS.traverseAnnSimpleSelect f s QRFPk s -> QRFPk <$> DS.traverseAnnSimpleSelect f s QRFSimple s -> QRFSimple <$> DS.traverseAnnSimpleSelect f s QRFAgg s -> QRFAgg <$> DS.traverseAnnAggregateSelect f s @@ -71,6 +74,7 @@ traverseQueryRootFldAST f = \case toPGQuery :: QueryRootFldResolved -> Q.Query toPGQuery = \case + QRFNode s -> Q.fromBuilder $ toSQL $ DS.mkSQLSelect DS.JASSingleObject s QRFPk s -> Q.fromBuilder $ toSQL $ DS.mkSQLSelect DS.JASSingleObject s QRFSimple s -> Q.fromBuilder $ toSQL $ DS.mkSQLSelect DS.JASMultipleRows s QRFAgg s -> Q.fromBuilder $ toSQL $ DS.mkAggregateSelect s @@ -106,6 +110,13 @@ queryFldToPGAST fld actionExecuter = do opCtx <- getOpCtx $ V._fName fld userInfo <- asks getter case opCtx of + QCNodeSelect nodeSelectMap -> do + NodeIdData table pkeyColumnValues <- RS.resolveNodeId fld + case Map.lookup (GS.mkTableTy table) nodeSelectMap of + Nothing -> throwVE $ "table " <> table <<> " not found" + Just selOpCtx -> do + validateHdrs userInfo (_socHeaders selOpCtx) + QRFNode <$> RS.convertNodeSelect selOpCtx pkeyColumnValues fld QCSelect ctx -> do validateHdrs userInfo (_socHeaders ctx) QRFSimple <$> RS.convertSelect ctx fld @@ -130,13 +141,11 @@ queryFldToPGAST fld actionExecuter = do -- an SQL query, but in case of query actions it's converted into JSON -- and included in the action's webhook payload. markNotReusable - let f = case jsonAggType of + let jsonAggType = RA.mkJsonAggSelect $ _saecOutputType ctx + f = case jsonAggType of DS.JASMultipleRows -> QRFActionExecuteList DS.JASSingleObject -> QRFActionExecuteObject f <$> actionExecuter (RA.resolveActionQuery fld ctx (_uiSession userInfo)) - where - outputType = _saecOutputType ctx - jsonAggType = RA.mkJsonAggSelect outputType QCSelectConnection pk ctx -> QRFConnection <$> RS.convertConnectionSelect pk ctx fld QCFuncConnection pk ctx -> @@ -200,6 +209,7 @@ getOpCtx f = do toSQLFromItem :: S.Alias -> QueryRootFldResolved -> S.FromItem toSQLFromItem alias = \case + QRFNode s -> fromSelect $ DS.mkSQLSelect DS.JASSingleObject s QRFPk s -> fromSelect $ DS.mkSQLSelect DS.JASSingleObject s QRFSimple s -> fromSelect $ DS.mkSQLSelect DS.JASMultipleRows s QRFAgg s -> fromSelect $ DS.mkAggregateSelect s diff --git a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs index 48680e47310b4..474eaed598d7f 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs @@ -169,6 +169,8 @@ parseColExp nt n val = do fmapAnnBoolExp partialSQLExpToUnresolvedVal permExp RFComputedField _ -> throw500 "computed fields are not allowed in bool_exp" + RFNodeId _ _ -> throw500 + "node id is not allowed in bool_exp" parseBoolExp :: ( MonadReusability m diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs index 0178184646e0d..3aac7d62389a7 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs @@ -33,21 +33,21 @@ module Hasura.GraphQL.Resolve.Context import Data.Has import Hasura.Prelude -import qualified Data.HashMap.Strict as Map -import qualified Data.Sequence as Seq -import qualified Database.PG.Query as Q -import qualified Language.GraphQL.Draft.Syntax as G +import qualified Data.HashMap.Strict as Map +import qualified Data.Sequence as Seq +import qualified Database.PG.Query as Q +import qualified Language.GraphQL.Draft.Syntax as G import Hasura.GraphQL.Resolve.Types import Hasura.GraphQL.Utils import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types -import Hasura.RQL.DML.Internal (currentSession, sessVarFromCurrentSetting) +import Hasura.RQL.DML.Internal (currentSession, sessVarFromCurrentSetting) import Hasura.RQL.Types import Hasura.SQL.Types import Hasura.SQL.Value -import qualified Hasura.SQL.DML as S +import qualified Hasura.SQL.DML as S getFldInfo :: (MonadError QErr m, MonadReader r m, Has FieldMap r) @@ -68,6 +68,7 @@ getPGColInfo nt n = do RFPGColumn pgColInfo -> return pgColInfo RFRelationship _ -> throw500 $ mkErrMsg "relation" RFComputedField _ -> throw500 $ mkErrMsg "computed field" + RFNodeId _ _ -> throw500 $ mkErrMsg "node id" where mkErrMsg ty = "found " <> ty <> " when expecting pgcolinfo for " diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs b/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs index 79d377b221c10..81d349442755d 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Introspect.hs @@ -44,7 +44,7 @@ withSubFields -> m J.Object withSubFields selSet fn = do objectSelectionSet <- asObjectSelectionSet selSet - fmap Map.fromList $ traverseObjectSelectionSet objectSelectionSet fn + Map.fromList <$> traverseObjectSelectionSet objectSelectionSet fn -- val <- fn fld -- return (G.unName $ G.unAlias $ _fAlias fld, val) @@ -146,7 +146,7 @@ ifaceR' => IFaceTyInfo -> Field -> m J.Object -ifaceR' i@(IFaceTyInfo descM n flds implementations) fld = do +ifaceR' i@(IFaceTyInfo descM n flds _) fld = do dummyReadIncludeDeprecated fld withSubFields (_fSelSet fld) $ \subFld -> case _fName subFld of diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index c02b345c3879a..e0aec9c388a68 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -8,6 +8,8 @@ module Hasura.GraphQL.Resolve.Select , convertFuncQueryAgg , parseColumns , processTableSelectionSet + , resolveNodeId + , convertNodeSelect , AnnSimpleSelect ) where @@ -24,7 +26,6 @@ import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.List.NonEmpty as NE import qualified Data.Sequence as Seq import qualified Data.Text as T -import qualified Data.Text.Conversions as TC import qualified Language.GraphQL.Draft.Syntax as G import qualified Hasura.RQL.DML.Select as RS @@ -34,6 +35,7 @@ import Hasura.GraphQL.Resolve.BoolExp import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Schema (isAggregateField) +import Hasura.GraphQL.Schema.Common (mkTableTy) import Hasura.GraphQL.Validate.SelectionSet import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Internal (onlyPositiveInt) @@ -113,6 +115,7 @@ processTableSelectionSet fldTy flds = _ -> do fldInfo <- getFldInfo fldTy fldName case fldInfo of + RFNodeId tn pkeys -> pure $ RS.AFNodeId tn pkeys RFPGColumn colInfo -> RS.mkAnnColumnField colInfo <$> argsToColumnOp (_fArguments fld) RFComputedField computedField -> @@ -550,8 +553,8 @@ parseConnectionArgs pKeyColumns args = do maybeSplit <- case (Map.lookup "after" args, Map.lookup "before" args) of (Nothing, Nothing) -> pure Nothing (Just _, Just _) -> throwVE "\"after\" and \"before\" are not allowed at once" - (Just v, Nothing) -> fmap ((RS.CSKAfter,) . TC.convertText) <$> asPGColTextM v - (Nothing, Just v) -> fmap ((RS.CSKBefore,) . TC.convertText) <$> asPGColTextM v + (Just v, Nothing) -> fmap ((RS.CSKAfter,) . base64Decode) <$> asPGColTextM v + (Nothing, Just v) -> fmap ((RS.CSKBefore,) . base64Decode) <$> asPGColTextM v let ordByExpM = NE.nonEmpty =<< ordByExpML tableArgs = RS.SelectArgs whereExpM ordByExpM Nothing Nothing Nothing @@ -562,12 +565,11 @@ parseConnectionArgs pKeyColumns args = do validateConnectionSplit :: Maybe (NonEmpty (RS.AnnOrderByItemG UnresolvedVal)) -> RS.ConnectionSplitKind - -> Maybe (TC.Base64 BL.ByteString) + -> BL.ByteString -> m (NonEmpty (RS.ConnectionSplit UnresolvedVal)) - validateConnectionSplit maybeOrderBys splitKind maybeCursorSplit = do - cursorSplit <- maybe throwInvalidCursor pure maybeCursorSplit + validateConnectionSplit maybeOrderBys splitKind cursorSplit = do cursorValue <- either (const throwInvalidCursor) pure $ - J.eitherDecode $ TC.unBase64 cursorSplit + J.eitherDecode cursorSplit case maybeOrderBys of Nothing -> forM pKeyColumns $ \pgColumnInfo -> do @@ -735,3 +737,54 @@ convertConnectionFuncQuery pkCols funcOpCtx fld = fromConnectionField selectFrom pkCols permFilter permLimit fld where FuncQOpCtx qf argSeq _ _ permFilter permLimit = funcOpCtx + +resolveNodeId + :: forall m. ( MonadError QErr m + , MonadReusability m + ) + => Field -> m NodeIdData +resolveNodeId field = + withPathK "selectionSet" $ fieldAsPath field $ do + nodeIdText <- asPGColText =<< getArg (_fArguments field) "id" + either (const throwInvalidNodeId) pure $ + J.eitherDecode $ base64Decode nodeIdText + where + throwInvalidNodeId = throwVE "the node id is invalid" + +convertNodeSelect + :: ( MonadReusability m + , MonadError QErr m + , MonadReader r m + , Has FieldMap r + , Has OrdByCtx r + , Has SQLGenCtx r + ) + => SelOpCtx + -> Map.HashMap PGCol J.Value + -> Field + -> m (RS.AnnSimpleSelG UnresolvedVal) +convertNodeSelect selOpCtx pkeyColumnValues field = + withPathK "selectionSet" $ fieldAsPath field $ do + -- Parse selection set as interface + ifaceSelectionSet <- asInterfaceSelectionSet $ _fSelSet field + let tableObjectType = mkTableTy table + selSet = getMemberSelectionSet tableObjectType ifaceSelectionSet + unresolvedPermFilter = fmapAnnBoolExp partialSQLExpToUnresolvedVal permFilter + tablePerm = RS.TablePerm unresolvedPermFilter permLimit + -- Resolve the table selection set + annFields <- processTableSelectionSet tableObjectType selSet + -- Resolve the Node id primary key column values + unresolvedPkeyValues <- flip Map.traverseWithKey pkeyColumnValues $ + \pgColumn jsonValue -> case Map.lookup pgColumn pgColumnMap of + Nothing -> throwVE $ "column " <> pgColumn <<> " not found" + Just columnInfo -> (,columnInfo) . UVPG . AnnPGVal Nothing False <$> + parsePGScalarValue (pgiType columnInfo) jsonValue + -- Generate the bool expression from the primary key column values + let pkeyBoolExp = BoolAnd $ flip map (Map.elems unresolvedPkeyValues) $ + \(unresolvedValue, columnInfo) -> (BoolFld . AVCol columnInfo) [AEQ True unresolvedValue] + selectArgs = RS.noSelectArgs{RS._saWhere = Just pkeyBoolExp} + strfyNum <- stringifyNum <$> asks getter + pure $ RS.AnnSelectG annFields (RS.FromTable table) tablePerm selectArgs strfyNum + where + SelOpCtx table _ allColumns permFilter permLimit = selOpCtx + pgColumnMap = mapFromL pgiColumn $ Map.elems allColumns diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs index 1e08b88961206..c07f19dc4911f 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Types.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Types.hs @@ -7,6 +7,9 @@ module Hasura.GraphQL.Resolve.Types import Control.Lens.TH import Hasura.Prelude +import qualified Data.Aeson as J +import qualified Data.Aeson.Casing as J +import qualified Data.Aeson.TH as J import qualified Data.HashMap.Strict as Map import qualified Data.Sequence as Seq import qualified Data.Text as T @@ -27,8 +30,11 @@ import Hasura.SQL.Value import qualified Hasura.SQL.DML as S +type NodeSelectMap = Map.HashMap G.NamedType SelOpCtx + data QueryCtx - = QCSelect !SelOpCtx + = QCNodeSelect !NodeSelectMap + | QCSelect !SelOpCtx | QCSelectConnection !(NonEmpty PGColumnInfo) !SelOpCtx | QCSelectPkey !SelPkOpCtx | QCSelectAgg !SelOpCtx @@ -174,6 +180,7 @@ data ResolveField = RFPGColumn !PGColumnInfo | RFRelationship !RelationshipField | RFComputedField !ComputedField + | RFNodeId !QualifiedTable !(NonEmpty PGColumnInfo) deriving (Show, Eq) type FieldMap = Map.HashMap (G.NamedType, G.Name) ResolveField @@ -253,6 +260,13 @@ data InputFunctionArgument | IFAUnknown !FunctionArgItem -- ^ Unknown value, need to be parsed deriving (Show, Eq) +data NodeIdData + = NodeIdData + { _nidTable :: !QualifiedTable + , _nidColumns :: !(Map.HashMap PGCol J.Value) + } deriving (Show, Eq) +$(J.deriveFromJSON (J.aesonDrop 4 J.snakeCase) ''NodeIdData) + -- template haskell related $(makePrisms ''ResolveField) $(makeLenses ''ComputedField) diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index d4b91b156d243..45415d3bf9fbc 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -253,7 +253,7 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi -- table obj selectObjects = case selPermM of Just (_, selFlds) -> - [ mkTableObj tn False descM selFlds + [ mkTableObj tn descM selFlds ] Nothing -> [] diff --git a/server/src-lib/Hasura/GraphQL/Schema/Common.hs b/server/src-lib/Hasura/GraphQL/Schema/Common.hs index dce7e0eb051ec..8c6a858c0ffb2 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Common.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Common.hs @@ -32,6 +32,9 @@ module Hasura.GraphQL.Schema.Common , numAggregateOps , compAggregateOps + + , nodeType + , nodeIdType ) where import qualified Data.HashMap.Strict as Map @@ -152,3 +155,11 @@ numAggregateOps = [ "sum", "avg", "stddev", "stddev_samp", "stddev_pop" compAggregateOps :: [G.Name] compAggregateOps = ["max", "min"] + +nodeType :: G.NamedType +nodeType = + G.NamedType "Node" + +nodeIdType :: G.GType +nodeIdType = + G.toGT $ G.toNT $ G.NamedType "ID" diff --git a/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs b/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs index c8771cb521454..a4cac50aa9a1d 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs @@ -65,9 +65,9 @@ mkTabAggregateOpOrdByInpObjs -> ([PGColumnInfo], [G.Name]) -> ([PGColumnInfo], [G.Name]) -> [InpObjTyInfo] -mkTabAggregateOpOrdByInpObjs tn (numCols, numAggregateOps) (compCols, compAggregateOps) = - mapMaybe (mkInpObjTyM numCols) numAggregateOps - <> mapMaybe (mkInpObjTyM compCols) compAggregateOps +mkTabAggregateOpOrdByInpObjs tn (numCols, numericAggregateOps) (compCols, compareAggregateOps) = + mapMaybe (mkInpObjTyM numCols) numericAggregateOps + <> mapMaybe (mkInpObjTyM compCols) compareAggregateOps where mkDesc (G.Name op) = @@ -97,15 +97,15 @@ mkTabAggOrdByInpObj -> ([PGColumnInfo], [G.Name]) -> ([PGColumnInfo], [G.Name]) -> InpObjTyInfo -mkTabAggOrdByInpObj tn (numCols, numAggregateOps) (compCols, compAggregateOps) = +mkTabAggOrdByInpObj tn (numCols, numericAggregateOps) (compCols, compareAggregateOps) = mkHsraInpTyInfo (Just desc) (mkTabAggOrdByTy tn) $ fromInpValL $ numOpOrdBys <> compOpOrdBys <> [countInpVal] where desc = G.Description $ "order by aggregate values of table " <>> tn - numOpOrdBys = bool (map mkInpValInfo numAggregateOps) [] $ null numCols - compOpOrdBys = bool (map mkInpValInfo compAggregateOps) [] $ null compCols + numOpOrdBys = bool (map mkInpValInfo numericAggregateOps) [] $ null numCols + compOpOrdBys = bool (map mkInpValInfo compareAggregateOps) [] $ null compCols mkInpValInfo op = InpValInfo Nothing op Nothing $ G.toGT $ mkTabAggregateOpOrdByTy tn op diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index f4a7d63004f51..d36239e3a581c 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -1,5 +1,6 @@ module Hasura.GraphQL.Schema.Select ( mkTableObj + , mkRelayTableObj , mkTableAggObj , mkSelColumnTy , mkTableAggregateFieldsObj @@ -206,11 +207,10 @@ type table { -} mkTableObj :: QualifiedTable - -> Bool -> Maybe PGDescription -> [SelField] -> ObjTyInfo -mkTableObj tn isRelay descM allowedFlds = +mkTableObj tn descM allowedFlds = mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) TLHasuraType where flds = pgColumnFields <> relFlds <> computedFlds @@ -218,7 +218,27 @@ mkTableObj tn isRelay descM allowedFlds = relFlds = concatMap mkRelationshipField' $ getRelationshipFields allowedFlds computedFlds = map mkComputedFieldFld $ getComputedFields allowedFlds mkRelationshipField' (RelationshipFieldInfo relInfo allowAgg _ _ _ maybePkCols isNullable) = - mkRelationshipField allowAgg relInfo isRelay maybePkCols isNullable + mkRelationshipField allowAgg relInfo False maybePkCols isNullable + desc = mkDescriptionWith descM $ "columns and relationships of " <>> tn + +mkRelayTableObj + :: QualifiedTable + -> Maybe PGDescription + -> [SelField] + -> ObjTyInfo +mkRelayTableObj tn descM allowedFlds = + mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) TLHasuraType + where + flds = nodeIdField:pgColumnFields <> relFlds <> computedFlds + nodeIdField = mkHsraObjFldInfo Nothing "id" mempty nodeIdType + pgColumnFields = map mkPGColFld $ + -- Remove "id" column + filter ((/=) "id" . getPGColTxt . pgiColumn) $ + getPGColumnFields allowedFlds + relFlds = concatMap mkRelationshipField' $ getRelationshipFields allowedFlds + computedFlds = map mkComputedFieldFld $ getComputedFields allowedFlds + mkRelationshipField' (RelationshipFieldInfo relInfo allowAgg _ _ _ maybePkCols isNullable) = + mkRelationshipField allowAgg relInfo True maybePkCols isNullable desc = mkDescriptionWith descM $ "columns and relationships of " <>> tn {- diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index 650d15099218a..9f18f6e55582f 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -8,7 +8,6 @@ import qualified Network.HTTP.Types as N import Hasura.EncJSON import Hasura.GraphQL.Logging -import Hasura.GraphQL.NormalForm import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.HTTP import Hasura.Prelude @@ -24,7 +23,6 @@ import qualified Hasura.Logging as L import qualified Hasura.Server.Telemetry.Counters as Telem import qualified Language.GraphQL.Draft.Syntax as G import qualified Network.HTTP.Types as HTTP -import qualified Language.GraphQL.Draft.Printer.Text as GP runGQ :: ( HasVersion @@ -49,17 +47,17 @@ runGQ reqId userInfo reqHdrs apiType req = do E.GExPHasura resolvedOp -> do (telemTimeIO, telemQueryType, respHdrs, resp) <- runHasuraGQ reqId req userInfo resolvedOp return (telemCacheHit, Telem.Local, (telemTimeIO, telemQueryType, HttpResponse resp respHdrs)) - E.GExPRemote rsi opDef rootSelSet -> do + E.GExPRemote rsi opDef _ -> do let telemQueryType | G._todType opDef == G.OperationTypeMutation = Telem.Mutation | otherwise = Telem.Query - rewrittenQuery = - GQLReq { _grQuery = GQLQueryText $ GP.renderExecutableDoc $ - G.ExecutableDocument $ pure $ - toGraphQLOperation rootSelSet - , _grVariables = Nothing - , _grOperationName = Nothing - } - (telemTimeIO, resp) <- E.execRemoteGQ reqId userInfo reqHdrs rewrittenQuery rsi opDef + -- rewrittenQuery = + -- GQLReq { _grQuery = GQLQueryText $ GP.renderExecutableDoc $ + -- G.ExecutableDocument $ pure $ + -- toGraphQLOperation rootSelSet + -- , _grVariables = Nothing + -- , _grOperationName = Nothing + -- } + (telemTimeIO, resp) <- E.execRemoteGQ reqId userInfo reqHdrs req rsi opDef return (telemCacheHit, Telem.Remote, (telemTimeIO, telemQueryType, resp)) let telemTimeIO = convertDuration telemTimeIO_DT telemTimeTot = convertDuration telemTimeTot_DT diff --git a/server/src-lib/Hasura/GraphQL/Validate.hs b/server/src-lib/Hasura/GraphQL/Validate.hs index 1804f5918e223..66d13c1bc0e71 100644 --- a/server/src-lib/Hasura/GraphQL/Validate.hs +++ b/server/src-lib/Hasura/GraphQL/Validate.hs @@ -22,7 +22,6 @@ import Data.Has import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict.InsOrd.Extended as OMap import qualified Data.HashSet as HS -import qualified Data.Sequence as Seq import qualified Language.GraphQL.Draft.Syntax as G import Hasura.GraphQL.Schema @@ -163,16 +162,16 @@ validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do case G._todType opDef of G.OperationTypeQuery -> return $ RQuery selSet G.OperationTypeMutation -> return $ RMutation selSet - G.OperationTypeSubscription -> do + G.OperationTypeSubscription -> case OMap.toList $ unAliasedFields $ unObjectSelectionSet selSet of - [] -> throw500 "empty selset for subscription" - ((alias, field):rst) -> do + [] -> throw500 "empty selset for subscription" + (_:rst) -> do -- As an internal testing feature, we support subscribing to multiple -- selection sets. First check if the corresponding directive is set. - let multipleAllowed = elem (G.Directive "_multiple_top_level_fields" []) (G._todDirectives opDef) + let multipleAllowed = G.Directive "_multiple_top_level_fields" [] `elem` G._todDirectives opDef unless (multipleAllowed || null rst) $ throwVE "subscriptions must select one top level field" - return $ RSubscription alias field + return $ RSubscription selSet isQueryInAllowlist :: GQLExecDoc -> HS.HashSet GQLQuery -> Bool isQueryInAllowlist q = HS.member gqlQuery diff --git a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs index 1a78840c26231..64b3972cd7c8c 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/SelectionSet.hs @@ -1,5 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilyDependencies #-} -{-# LANGUAGE RecordWildCards #-} module Hasura.GraphQL.Validate.SelectionSet ( ArgsMap , Field(..) @@ -12,22 +12,24 @@ module Hasura.GraphQL.Validate.SelectionSet , RootSelectionSet(..) , parseObjectSelectionSet , asObjectSelectionSet + , asInterfaceSelectionSet + , getMemberSelectionSet ) where import Hasura.Prelude -import qualified Data.Text as T import qualified Data.HashMap.Strict as Map -import qualified Data.HashSet as Set import qualified Data.HashMap.Strict.InsOrd.Extended as OMap -import qualified Language.GraphQL.Draft.Syntax as G -import qualified Data.Sequence.NonEmpty as NE +import qualified Data.HashSet as Set import qualified Data.List as L +import qualified Data.Sequence.NonEmpty as NE +import qualified Data.Text as T +import qualified Language.GraphQL.Draft.Syntax as G +import Hasura.GraphQL.NormalForm import Hasura.GraphQL.Validate.Context -import Hasura.GraphQL.Validate.Types import Hasura.GraphQL.Validate.InputValue -import Hasura.GraphQL.NormalForm +import Hasura.GraphQL.Validate.Types import Hasura.RQL.Types import Hasura.SQL.Value @@ -116,9 +118,12 @@ parseSelectionSet => a -> G.SelectionSet -> m (NormalizedSelectionSet a) -parseSelectionSet fieldTypeInfo selectionSet = +parseSelectionSet fieldTypeInfo selectionSet = do + visitedFragments <- get withPathK "selectionSet" $ do - normalizedSelections <- catMaybes <$> mapM (parseSelection fieldTypeInfo) selectionSet + -- The visited fragments state shouldn't accumulate over a selection set. + normalizedSelections <- + catMaybes <$> mapM (parseSelection visitedFragments fieldTypeInfo) selectionSet mergeNormalizedSelections normalizedSelections where mergeNormalizedSelections = mergeNormalizedSelectionSets . map selectionToSelectionSet @@ -129,27 +134,28 @@ parseSelection :: ( MonadReader ValidationCtx m , MonadError QErr m , MonadReusability m - , MonadState [G.Name] m , HasSelectionSet a ) - => a -- parent type info + => [G.Name] + -> a -- parent type info -> G.Selection -> m (Maybe (NormalizedSelection a)) -parseSelection parentTypeInfo = \case - G.SelectionField fld -> withPathK (G.unName $ G._fName fld) $ do - let fieldName = G._fName fld - fieldAlias = fromMaybe (G.Alias fieldName) $ G._fAlias fld - fmap (SelectionField fieldAlias) <$> parseField_ parentTypeInfo fld - G.SelectionFragmentSpread (G.FragmentSpread name directives) -> do - FragDef _ fragmentTyInfo fragmentSelectionSet <- getFragmentInfo name - withPathK (G.unName name) $ - fmap (SelectionFragmentSpread name) <$> - parseFragment parentTypeInfo fragmentTyInfo directives fragmentSelectionSet - G.SelectionInlineFragment (G.InlineFragment {..}) -> do - let fragmentType = fromMaybe (getTypename parentTypeInfo) _ifTypeCondition - fragmentTyInfo <- getFragmentTyInfo fragmentType - withPathK "inlineFragment" $ fmap SelectionInlineFragmentSpread <$> - parseFragment parentTypeInfo fragmentTyInfo _ifDirectives _ifSelectionSet +parseSelection visitedFragments parentTypeInfo = + flip evalStateT visitedFragments . \case + G.SelectionField fld -> withPathK (G.unName $ G._fName fld) $ do + let fieldName = G._fName fld + fieldAlias = fromMaybe (G.Alias fieldName) $ G._fAlias fld + fmap (SelectionField fieldAlias) <$> parseField_ parentTypeInfo fld + G.SelectionFragmentSpread (G.FragmentSpread name directives) -> do + FragDef _ fragmentTyInfo fragmentSelectionSet <- getFragmentInfo name + withPathK (G.unName name) $ + fmap (SelectionFragmentSpread name) <$> + parseFragment parentTypeInfo fragmentTyInfo directives fragmentSelectionSet + G.SelectionInlineFragment G.InlineFragment{..} -> do + let fragmentType = fromMaybe (getTypename parentTypeInfo) _ifTypeCondition + fragmentTyInfo <- getFragmentTyInfo fragmentType + withPathK "inlineFragment" $ fmap SelectionInlineFragmentSpread <$> + parseFragment parentTypeInfo fragmentTyInfo _ifDirectives _ifSelectionSet parseFragment :: ( MonadReader ValidationCtx m @@ -193,13 +199,13 @@ parseFragment parentTyInfo fragmentTyInfo directives fragmentSelectionSet = do parentTypeMembers = getMemberTypes parentTyInfo fragmentType = case fragmentTyInfo of - FragmentTyObject tyInfo -> getTypename tyInfo + FragmentTyObject tyInfo -> getTypename tyInfo FragmentTyInterface tyInfo -> getTypename tyInfo - FragmentTyUnion tyInfo -> getTypename tyInfo + FragmentTyUnion tyInfo -> getTypename tyInfo fragmentTypeMembers = case fragmentTyInfo of - FragmentTyObject tyInfo -> getMemberTypes tyInfo + FragmentTyObject tyInfo -> getMemberTypes tyInfo FragmentTyInterface tyInfo -> getMemberTypes tyInfo - FragmentTyUnion tyInfo -> getMemberTypes tyInfo + FragmentTyUnion tyInfo -> getMemberTypes tyInfo class IsField f => MergeableField f where @@ -542,4 +548,3 @@ instance HasSelectionSet UnionTyInfo where ScopedSelectionSet (AliasedFields mempty) $ Map.fromList $ flip map (toList commonTypes) $ \commonType -> (commonType, getMemberSelectionSet commonType unionSelectionSet) - diff --git a/server/src-lib/Hasura/GraphQL/Validate/Types.hs b/server/src-lib/Hasura/GraphQL/Validate/Types.hs index 80cd32b53780c..55fb217841753 100644 --- a/server/src-lib/Hasura/GraphQL/Validate/Types.hs +++ b/server/src-lib/Hasura/GraphQL/Validate/Types.hs @@ -262,7 +262,7 @@ instance Semigroup ObjTyInfo where mkObjTyInfo :: Maybe G.Description -> G.NamedType -> IFacesSet -> ObjFieldMap -> TypeLoc -> ObjTyInfo -mkObjTyInfo descM ty iFaces flds loc = +mkObjTyInfo descM ty iFaces flds _ = ObjTyInfo descM ty iFaces $ Map.insert (_fiName newFld) newFld flds where newFld = typenameFld @@ -278,7 +278,7 @@ mkHsraObjTyInfo descM ty implIFaces flds = mkIFaceTyInfo :: Maybe G.Description -> G.NamedType -> Map.HashMap G.Name ObjFldInfo -> TypeLoc -> MemberTypes -> IFaceTyInfo -mkIFaceTyInfo descM ty flds loc = +mkIFaceTyInfo descM ty flds _ = IFaceTyInfo descM ty $ Map.insert (_fiName newFld) newFld flds where newFld = typenameFld diff --git a/server/src-lib/Hasura/Prelude.hs b/server/src-lib/Hasura/Prelude.hs index 55805fb19d049..bbdea55efe15f 100644 --- a/server/src-lib/Hasura/Prelude.hs +++ b/server/src-lib/Hasura/Prelude.hs @@ -10,6 +10,7 @@ module Hasura.Prelude , afold , bsToTxt , txtToBs + , base64Decode , spanMaybeM , findWithIndex , mapFromL @@ -62,6 +63,9 @@ import Test.QuickCheck.Arbitrary.Generic as M import Text.Read as M (readEither, readMaybe) import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL + +import qualified Data.ByteString.Base64.Lazy as Base64 import qualified Data.HashMap.Strict as Map import qualified Data.Text as T import qualified Data.Text.Encoding as TE @@ -99,6 +103,11 @@ bsToTxt = TE.decodeUtf8With TE.lenientDecode txtToBs :: Text -> B.ByteString txtToBs = TE.encodeUtf8 +base64Decode :: Text -> BL.ByteString +base64Decode = + Base64.decodeLenient . BL.fromStrict . txtToBs + + -- Like 'span', but monadic and with a function that produces 'Maybe' instead of 'Bool' spanMaybeM :: (Foldable f, Monad m) diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index e80ca5c9e53ef..2517b5c565d7a 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -272,13 +272,13 @@ selectP2 jsonAggSelect (sel, p) = where selectSQL = toSQL $ mkSQLSelect jsonAggSelect sel -selectQuerySQL :: JsonAggSelect -> AnnSimpleSel -> Q.Query -selectQuerySQL jsonAggSelect sel = - Q.fromBuilder $ toSQL $ mkSQLSelect jsonAggSelect sel +-- selectQuerySQL :: JsonAggSelect -> AnnSimpleSel -> Q.Query +-- selectQuerySQL jsonAggSelect sel = +-- Q.fromBuilder $ toSQL $ mkSQLSelect jsonAggSelect sel -selectAggQuerySQL :: AnnAggregateSelect -> Q.Query -selectAggQuerySQL = - Q.fromBuilder . toSQL . mkAggregateSelect +-- selectAggQuerySQL :: AnnAggregateSelect -> Q.Query +-- selectAggQuerySQL = +-- Q.fromBuilder . toSQL . mkAggregateSelect asSingleRowJsonResp :: Q.Query -> [Q.PrepArg] -> Q.TxE QErr EncJSON asSingleRowJsonResp query args = diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index 9766fc5cf7af2..b64d0f6efecf6 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -727,13 +727,15 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do case field of AFExpression t -> pure $ S.SELit t + AFNodeId tn pKeys -> pure $ mkNodeId tn pKeys + AFColumn c -> toSQLCol c AFObjectRelation objSel -> withWriteObjectRelation $ do let AnnRelationSelectG relName relMapping annSel = objSel objRelSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName (selectSource, extractors) <- processAnnSimpleSelect (mkSourcePrefixes objRelSourcePrefix) - fieldName PLSQNotRequired annSel + fieldName PLSQNotRequired annSel let objRelSource = ObjectRelationSource relName relMapping selectSource pure ( objRelSource , extractors @@ -796,6 +798,19 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do Nothing -> sqlExp Just (ColumnOp opText cExp) -> S.mkSQLOpExp opText sqlExp cExp + mkNodeId :: QualifiedTable -> NonEmpty PGColumnInfo -> S.SQLExp + mkNodeId (QualifiedObject tableSchema tableName) pkeyColumns = + let tableObjectExp = S.applyJsonBuildObj + [ S.SELit "schema" + , S.SELit (getSchemaTxt tableSchema) + , S.SELit "name" + , S.SELit (toTxt tableName) + ] + in encodeBase64 $ flip S.SETyAnn S.textTypeAnn $ S.applyJsonBuildObj + [ S.SELit "table", tableObjectExp + , S.SELit "columns", mkPrimaryKeyColumnsObjectExp sourcePrefix pkeyColumns + ] + injectJoinCond :: S.BoolExp -- ^ Join condition -> S.BoolExp -- ^ Where condition -> S.WhereFrag -- ^ New where frag @@ -991,6 +1006,22 @@ pageInfoSelectAliasIden = Iden "__page_info" cursorsSelectAliasIden :: Iden cursorsSelectAliasIden = Iden "__cursors_select" +mkPrimaryKeyColumnsObjectExp :: Iden -> NonEmpty PGColumnInfo -> S.SQLExp +mkPrimaryKeyColumnsObjectExp sourcePrefix primaryKeyColumns = + S.applyJsonBuildObj $ flip concatMap (toList primaryKeyColumns) $ + \pgColumnInfo -> + [ S.SELit $ getPGColTxt $ pgiColumn pgColumnInfo + , toJSONableExp False (pgiType pgColumnInfo) False $ + S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ pgiColumn pgColumnInfo + ] + +encodeBase64 :: S.SQLExp -> S.SQLExp +encodeBase64 t = + S.SEFnApp "encode" + [S.SEFnApp "convert_to" [t, S.SELit "UTF8"] Nothing, S.SELit "base64"] + Nothing + + processConnectionSelect :: ( MonadReader Bool m , MonadWriter JoinTree m @@ -1015,7 +1046,7 @@ processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connection Nothing -> -- Extract primary key columns from base select along with cursor expression. -- Those columns are required to perform connection split via a WHERE clause. - mkCursorExtractor primaryKeyColumnsCursor : primaryKeyColumnExtractors + mkCursorExtractor (mkPrimaryKeyColumnsObjectExp thisPrefix primaryKeyColumns) : primaryKeyColumnExtractors orderByExp = _ssOrderBy selectSource (topExtractorExp, exps) <- flip runStateT [] $ processFields orderByExp let topExtractor = S.Extractor topExtractorExp $ Just $ S.Alias fieldIden @@ -1033,14 +1064,6 @@ processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connection thisPrefix = _pfThis sourcePrefixes permLimitSubQuery = PLSQNotRequired - primaryKeyColumnsCursor = - S.applyJsonBuildObj $ flip concatMap (toList primaryKeyColumns) $ - \pgColumnInfo -> - [ S.SELit $ getPGColTxt $ pgiColumn pgColumnInfo - , toJSONableExp False (pgiType pgColumnInfo) False $ - S.mkQIdenExp (mkBaseTableAlias thisPrefix) $ pgiColumn pgColumnInfo - ] - primaryKeyColumnExtractors = flip map (toList primaryKeyColumns) $ \pgColumnInfo -> @@ -1073,10 +1096,6 @@ processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connection EdgeNode annFields -> mkSimilarArrayFields annFields $ _saOrderBy tableArgs - encodeBase64 t = S.SEFnApp "encode" - [S.SETyAnn t $ S.TypeAnn "bytea", S.SELit "base64"] - Nothing - mkSimpleJsonAgg rowExp ob = let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing diff --git a/server/src-lib/Hasura/RQL/DML/Select/Types.hs b/server/src-lib/Hasura/RQL/DML/Select/Types.hs index fc4e6fa1cf81e..b819119007151 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Types.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Types.hs @@ -175,6 +175,7 @@ data AnnFieldG v | AFObjectRelation !(ObjectRelationSelectG v) | AFArrayRelation !(ArraySelectG v) | AFComputedField !(ComputedFieldSelect v) + | AFNodeId !QualifiedTable !(NonEmpty PGColumnInfo) | AFExpression !T.Text deriving (Show, Eq) @@ -194,6 +195,7 @@ traverseAnnField f = \case AFObjectRelation sel -> AFObjectRelation <$> traverse (traverseAnnSimpleSelect f) sel AFArrayRelation sel -> AFArrayRelation <$> traverseArraySelect f sel AFComputedField sel -> AFComputedField <$> traverseComputedFieldSelect f sel + AFNodeId qt pKeys -> pure $ AFNodeId qt pKeys AFExpression t -> AFExpression <$> pure t type AnnField = AnnFieldG S.SQLExp diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index d15827233ddc9..d6dfff2474401 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -11,7 +11,6 @@ import Language.Haskell.TH.Syntax (Lift, Q, TExp) import System.Environment import System.Exit import System.Process -import Data.Aeson.Internal import qualified Data.ByteString as B import qualified Data.CaseInsensitive as CI @@ -29,7 +28,6 @@ import qualified Network.Wreq as Wreq import qualified Text.Regex.TDFA as TDFA import qualified Text.Regex.TDFA.ReadRegex as TDFA import qualified Text.Regex.TDFA.TDFA as TDFA -import qualified Data.Vector as V import Hasura.RQL.Instances () diff --git a/server/tests-py/queries/graphql_query/basic/select_query_fragment_cycles.yaml b/server/tests-py/queries/graphql_query/basic/select_query_fragment_cycles.yaml index 3f44e6ba1beec..e66c929083178 100644 --- a/server/tests-py/queries/graphql_query/basic/select_query_fragment_cycles.yaml +++ b/server/tests-py/queries/graphql_query/basic/select_query_fragment_cycles.yaml @@ -25,6 +25,6 @@ query: response: errors: - extensions: - path: $.selectionSet.author.selectionSet.authorFragment.selectionSet.articles.selectionSet.articleFragment.selectionSet.author.selectionSet.authorFragment + path: $.selectionSet.author.selectionSet.authorFragment.selectionSet.articles.selectionSet.articleFragment.selectionSet.author.selectionSet code: validation-failed message: cannot spread fragment "authorFragment" within itself via articleFragment,authorFragment diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index 642d3148e995f..7bd7985cc7c1c 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -364,10 +364,10 @@ def test_remote_query_error(self, ws_client): try: ev = next(resp) print(ev) - assert ev['type'] == 'data' and ev['id'] == query_id, ev + assert ev['type'] == 'error' and ev['id'] == query_id, ev assert 'errors' in ev['payload'] assert ev['payload']['errors'][0]['message'] == \ - 'Cannot query field "blah" on type "User".' + 'field "blah" not found in type: \'User\'' finally: ws_client.stop(query_id) From 45cc7fe8ec9b7f0b62b0ea2d06aff429815a8dcf Mon Sep 17 00:00:00 2001 From: Rishichandra Wawhal Date: Tue, 26 May 2020 22:40:14 +0530 Subject: [PATCH 11/29] add relay toggle on graphiql --- console/src/Endpoints.js | 1 + .../components/Common/utils/graphqlUtils.ts | 10 +++- .../Services/ApiExplorer/Actions.js | 34 +++++++++-- .../ApiExplorer/Analyzer/AnalyzeButton.js | 10 ++-- .../Services/ApiExplorer/ApiExplorer.js | 5 ++ .../Services/ApiExplorer/ApiExplorer.scss | 5 ++ .../ApiExplorer/ApiRequest/ApiRequest.js | 35 +++++++++++- .../Services/ApiExplorer/ApiRequestWrapper.js | 2 + .../GraphiQLWrapper/GraphiQLWrapper.js | 39 +++++++++---- .../OneGraphExplorer/OneGraphExplorer.js | 28 +++++++--- .../components/Services/ApiExplorer/state.js | 56 +++++++++---------- .../components/Services/ApiExplorer/utils.js | 5 ++ 12 files changed, 169 insertions(+), 61 deletions(-) diff --git a/console/src/Endpoints.js b/console/src/Endpoints.js index e6902b4beb580..cfce4f48b5a11 100644 --- a/console/src/Endpoints.js +++ b/console/src/Endpoints.js @@ -10,6 +10,7 @@ const Endpoints = { getSchema: `${baseUrl}/v1/query`, serverConfig: `${baseUrl}/v1alpha1/config`, graphQLUrl: `${baseUrl}/v1/graphql`, + relayURL: `${baseUrl}/v1/relay`, schemaChange: `${baseUrl}/v1/query`, query: `${baseUrl}/v1/query`, rawSQL: `${baseUrl}/v1/query`, diff --git a/console/src/components/Common/utils/graphqlUtils.ts b/console/src/components/Common/utils/graphqlUtils.ts index d7591291dfb3a..b980febd4a41a 100644 --- a/console/src/components/Common/utils/graphqlUtils.ts +++ b/console/src/components/Common/utils/graphqlUtils.ts @@ -6,6 +6,14 @@ import { import React from 'react'; import endpoints from '../../../Endpoints'; +export const getGraphQLQueryPayload = ( + query: string, + variables: Record +) => ({ + query, + variables, +}); + export const useIntrospectionSchema = (headers = {}) => { const [schema, setSchema] = React.useState(null); const [loading, setLoading] = React.useState(true); @@ -17,7 +25,7 @@ export const useIntrospectionSchema = (headers = {}) => { fetch(endpoints.graphQLUrl, { method: 'POST', headers, - body: JSON.stringify({ query: getIntrospectionQuery() }), + body: JSON.stringify(getGraphQLQueryPayload(getIntrospectionQuery(), {})), }) .then(r => r.json()) .then(response => { diff --git a/console/src/components/Services/ApiExplorer/Actions.js b/console/src/components/Services/ApiExplorer/Actions.js index f8e5dd01e762f..8bbb0998657c6 100644 --- a/console/src/components/Services/ApiExplorer/Actions.js +++ b/console/src/components/Services/ApiExplorer/Actions.js @@ -9,7 +9,7 @@ import { WebSocketLink } from 'apollo-link-ws'; import { parse } from 'graphql'; import { execute } from 'apollo-link'; -import { getHeadersAsJSON } from './utils'; +import { getHeadersAsJSON, getGraphQLEndpoint } from './utils'; import { saveAppState, clearState } from '../../AppState.js'; import { ADMIN_SECRET_HEADER_KEY } from '../../../constants'; @@ -44,6 +44,18 @@ const CREATE_WEBSOCKET_CLIENT = 'ApiExplorer/CREATE_WEBSOCKET_CLIENT'; const FOCUS_ROLE_HEADER = 'ApiExplorer/FOCUS_ROLE_HEADER'; const UNFOCUS_ROLE_HEADER = 'ApiExplorer/UNFOCUS_ROLE_HEADER'; +const LOADING = 'ApiExplorer/LOADING'; +export const setLoading = isLoading => ({ + type: LOADING, + data: isLoading, +}); + +const SWITCH_GRAPHIQL_MODE = 'ApiExplorer/SWITCH_GRAPHIQL_MODE'; +export const switchGraphiQLMode = mode => ({ + type: SWITCH_GRAPHIQL_MODE, + mode, +}); + const clearHistory = () => { return { type: CLEAR_HISTORY, @@ -88,8 +100,9 @@ const getChangedHeaders = (headers, changedHeaderDetails) => { return nonEmptyHeaders; }; -const verifyJWTToken = token => dispatch => { - const url = Endpoints.graphQLUrl; +const verifyJWTToken = token => (dispatch, getState) => { + const { mode: graphiqlMode } = getState().apiexplorer; + const url = getGraphQLEndpoint(graphiqlMode); const body = { query: '{ __type(name: "dummy") {name}}', variables: null, @@ -199,10 +212,11 @@ const graphQLFetcherFinal = (graphQLParams, url, headers) => { }; /* Analyse Fetcher */ -const analyzeFetcher = (url, headers) => { +const analyzeFetcher = (headers, mode) => { return query => { const editedQuery = { query, + is_relay: mode === 'relay', }; const user = { @@ -227,7 +241,7 @@ const analyzeFetcher = (url, headers) => { editedQuery.user = user; - return fetch(`${url}/explain`, { + return fetch(`${Endpoints.graphQLUrl}/explain`, { method: 'post', headers: reqHeaders, body: JSON.stringify(editedQuery), @@ -616,6 +630,16 @@ const apiExplorerReducer = (state = defaultState, action) => { }, }, }; + case SWITCH_GRAPHIQL_MODE: + return { + ...state, + mode: action.mode, + }; + case LOADING: + return { + ...state, + loading: action.data, + }; default: return state; } diff --git a/console/src/components/Services/ApiExplorer/Analyzer/AnalyzeButton.js b/console/src/components/Services/ApiExplorer/Analyzer/AnalyzeButton.js index 99acffa306cee..e1d76e42428dd 100644 --- a/console/src/components/Services/ApiExplorer/Analyzer/AnalyzeButton.js +++ b/console/src/components/Services/ApiExplorer/Analyzer/AnalyzeButton.js @@ -4,6 +4,7 @@ import QueryAnalyzer from './QueryAnalyzer'; import GraphiQL from 'graphiql'; import { print, parse } from 'graphql'; import { isValidGraphQLOperation } from '../utils'; +import { getGraphQLQueryPayload } from '../../../Common/utils/graphqlUtils'; export default class AnalyseButton extends React.Component { constructor(props) { @@ -20,7 +21,7 @@ export default class AnalyseButton extends React.Component { }; } render() { - const operations = this.props.operations; + const { operations, mode } = this.props; const optionsOpen = this.state.optionsOpen; const hasOptions = operations && operations.length > 1; @@ -67,6 +68,7 @@ export default class AnalyseButton extends React.Component { {this.state.analyseQuery && ( @@ -105,6 +109,7 @@ ApiExplorer.propTypes = { tables: PropTypes.array.isRequired, headerFocus: PropTypes.bool.isRequired, location: PropTypes.object.isRequired, + mode: PropTypes.string.isRequired, }; const generatedApiExplorer = connect => { diff --git a/console/src/components/Services/ApiExplorer/ApiExplorer.scss b/console/src/components/Services/ApiExplorer/ApiExplorer.scss index 9c815fb5f4ef7..912442f4129e7 100644 --- a/console/src/components/Services/ApiExplorer/ApiExplorer.scss +++ b/console/src/components/Services/ApiExplorer/ApiExplorer.scss @@ -827,3 +827,8 @@ label { } } } + +.graphiqlModeToggle { + margin-top: -10px; + float: right; +} \ No newline at end of file diff --git a/console/src/components/Services/ApiExplorer/ApiRequest/ApiRequest.js b/console/src/components/Services/ApiExplorer/ApiRequest/ApiRequest.js index d52404aa7b542..2b60f9ef33849 100644 --- a/console/src/components/Services/ApiExplorer/ApiRequest/ApiRequest.js +++ b/console/src/components/Services/ApiExplorer/ApiRequest/ApiRequest.js @@ -1,12 +1,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; - import jwt from 'jsonwebtoken'; import TextAreaWithCopy from '../../../Common/TextAreaWithCopy/TextAreaWithCopy'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import Tooltip from 'react-bootstrap/lib/Tooltip'; import Modal from '../../../Common/Modal/Modal'; +import TooltipCustom from '../../../Common/Tooltip/Tooltip'; import { changeRequestHeader, @@ -15,9 +15,11 @@ import { unfocusTypingHeader, verifyJWTToken, setHeadersBulk, + switchGraphiQLMode, } from '../Actions'; import GraphiQLWrapper from '../GraphiQLWrapper/GraphiQLWrapper'; +import Toggle from '../../../Common/Toggle/Toggle'; import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle'; @@ -35,6 +37,7 @@ import { persistAdminSecretHeaderWasAdded, removePersistedAdminSecretHeaderWasAdded, } from './utils'; +import { getGraphQLEndpoint } from '../utils'; import styles from '../ApiExplorer.scss'; import { ADMIN_SECRET_HEADER_KEY } from '../../../../constants'; @@ -207,6 +210,7 @@ class ApiRequest extends Component { } render() { + const { mode, dispatch, loading } = this.props; const { isAnalyzingToken, tokenInfo, analyzingHeaderRow } = this.state; const { is_jwt_set: isJWTSet = false } = this.props.serverConfig; @@ -227,6 +231,11 @@ class ApiRequest extends Component { this.setState({ endpointSectionIsOpen: newIsOpen }); }; + const toggleGraphiqlMode = () => { + if (loading) return; + dispatch(switchGraphiQLMode(mode === 'relay' ? 'graphql' : 'relay')); + }; + return (
+
+ + Relay API + +
); @@ -564,6 +592,7 @@ class ApiRequest extends Component { return (
{ if (headerFocus) { @@ -61,14 +70,14 @@ class GraphiQLWrapper extends Component { return graphQLFetcherFinal( graphQLParams, - graphqlNetworkData.url, + getGraphQLEndpoint(mode), graphqlNetworkData.headers ); }; const analyzeFetcherInstance = analyzeFetcher( - graphqlNetworkData.url, - graphqlNetworkData.headers + graphqlNetworkData.headers, + mode ); let graphiqlContext; @@ -170,12 +179,14 @@ class GraphiQLWrapper extends Component { onClick: () => window.open(voyagerUrl, '_blank'), icon: