mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
bd6f7cd756
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3484 GitOrigin-RevId: 0f49dd7f64e80dfe6940d673afa76f12f6599126
225 lines
8.9 KiB
Haskell
225 lines
8.9 KiB
Haskell
module Hasura.GraphQL.Analyse
|
|
( Analysis (..),
|
|
FieldAnalysis (..),
|
|
FieldDef (..),
|
|
analyzeGraphqlQuery,
|
|
)
|
|
where
|
|
|
|
import Data.HashMap.Strict qualified as Map
|
|
import Data.HashMap.Strict.InsOrd qualified as OMap
|
|
import Hasura.Prelude hiding (get, put)
|
|
import Hasura.RQL.Types.RemoteSchema
|
|
( RemoteSchemaInputValueDefinition (RemoteSchemaInputValueDefinition, _rsitdDefinition),
|
|
RemoteSchemaIntrospection (RemoteSchemaIntrospection),
|
|
)
|
|
import Language.GraphQL.Draft.Syntax (ExecutableDefinition, Field, GType, Name, TypeDefinition)
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
|
|
|
-- | Analysis and FieldAnalysis are almost similar, except, Analysis will have a bool field (in future)
|
|
-- to indicate whether the GQL query is valid or not
|
|
data Analysis var = Analysis
|
|
{ -- | ordered hashmap from the fields to it's definition and analysis
|
|
_aFields :: InsOrdHashMap FieldName (FieldDef, Maybe (FieldAnalysis var)),
|
|
-- | map of all the variables, their type and default value (if there is any)
|
|
_aVars :: HashMap VarName (GType, Maybe (G.Value Void)),
|
|
_aErrs :: [Text]
|
|
}
|
|
deriving (Show)
|
|
|
|
data FieldAnalysis var = FieldAnalysis
|
|
{ _fFields :: InsOrdHashMap FieldName (FieldDef, Maybe (FieldAnalysis var)),
|
|
_fVars :: HashMap VarName (GType, Maybe (G.Value var)),
|
|
_fErrs :: [Text]
|
|
}
|
|
deriving (Show)
|
|
|
|
instance Monoid (FieldAnalysis v) where
|
|
mempty = FieldAnalysis mempty mempty []
|
|
|
|
instance Semigroup (FieldAnalysis v) where
|
|
(<>) fa1 fa2 = FieldAnalysis unionFields (_fVars fa1 <> _fVars fa2) (_fErrs fa1 <> _fErrs fa2)
|
|
where
|
|
unionFields = foldl' (safeInsertInFieldMap) (_fFields fa1) (OMap.toList (_fFields fa2))
|
|
|
|
instance Monoid (Analysis v) where
|
|
mempty = Analysis mempty mempty []
|
|
|
|
instance Semigroup (Analysis v) where
|
|
(<>) fa1 fa2 = Analysis unionFields (_aVars fa1 <> _aVars fa2) (_aErrs fa1 <> _aErrs fa2)
|
|
where
|
|
unionFields = foldl' (safeInsertInFieldMap) (_aFields fa1) (OMap.toList (_aFields fa2))
|
|
|
|
-- | FieldDef is analogous to `GType` from the `Language.GraphQL.Draft.Syntax` module
|
|
data FieldDef = FieldInfo G.Nullability (TypeDefinition [Name] RemoteSchemaInputValueDefinition) | FieldList G.Nullability FieldDef deriving (Show)
|
|
|
|
type FieldName = Name
|
|
|
|
type VarName = Name
|
|
|
|
-- | inserts in field map, if there is already a key, then take a union of the fields inside
|
|
-- i.e. for the following graphql query:
|
|
-- query MyQuery {
|
|
-- test {
|
|
-- a
|
|
-- b
|
|
-- }
|
|
-- test {
|
|
-- b
|
|
-- c
|
|
-- }
|
|
-- }
|
|
-- The final field map of `test` will have a, b and c
|
|
safeInsertInFieldMap ::
|
|
(Eq k, Hashable k, Semigroup a1) =>
|
|
InsOrdHashMap k (a2, Maybe a1) ->
|
|
(k, (a2, Maybe a1)) ->
|
|
InsOrdHashMap k (a2, Maybe a1)
|
|
safeInsertInFieldMap m (k, v) =
|
|
OMap.insertWith (\(fdef, (f1)) (_, (f2)) -> (fdef, f1 <> f2)) k v m
|
|
|
|
analyzeGraphqlQuery :: ExecutableDefinition Name -> RemoteSchemaIntrospection -> Maybe (Analysis Name)
|
|
analyzeGraphqlQuery (G.ExecutableDefinitionOperation (G.OperationDefinitionTyped td)) sc = do
|
|
let t = (G._todType td,) <$> G._todSelectionSet td
|
|
varDefs = G._todVariableDefinitions td
|
|
varMap = foldr (\G.VariableDefinition {..} m -> Map.insert _vdName (_vdType, _vdDefaultValue) m) mempty varDefs
|
|
(fieldMap, errs) = getFieldsMap sc t
|
|
pure Analysis {_aFields = fieldMap, _aVars = varMap, _aErrs = errs}
|
|
analyzeGraphqlQuery _ _ = Nothing
|
|
|
|
getFieldName :: Field frag var -> Name
|
|
getFieldName f = fromMaybe (G._fName f) (G._fAlias f)
|
|
|
|
getFieldsMap ::
|
|
RemoteSchemaIntrospection ->
|
|
[(G.OperationType, G.Selection frag var)] ->
|
|
(InsOrdHashMap FieldName (FieldDef, Maybe (FieldAnalysis var)), [Text])
|
|
getFieldsMap rs ss =
|
|
foldl'
|
|
( \(m, e) (o, f) -> case lookupRoot rs (o, f) of
|
|
Left x0 -> (safeInsertInFieldMap m (getFieldName f, x0), e)
|
|
Right txts -> (m, e <> txts)
|
|
)
|
|
(OMap.empty, [])
|
|
fields
|
|
where
|
|
fields = mapMaybe (\(o, s) -> (o,) <$> field s) ss
|
|
|
|
lookupRoot ::
|
|
RemoteSchemaIntrospection ->
|
|
(G.OperationType, G.Field frag var) ->
|
|
Either (FieldDef, Maybe (FieldAnalysis var)) [Text]
|
|
lookupRoot rs (ot, f) = do
|
|
let rootFieldName = getRootFieldNameFromOpType ot
|
|
fieldName = G._fName f
|
|
fieldTypeM = do
|
|
operationDefinitionSum <- lookupRS rs rootFieldName
|
|
operationDefinitionObject <- asObjectTypeDefinition operationDefinitionSum
|
|
fieldDefinition <- find ((== fieldName) . G._fldName) $ G._otdFieldsDefinition operationDefinitionObject
|
|
pure $ G._fldType fieldDefinition
|
|
|
|
case fieldTypeM of
|
|
Nothing -> Right $ ["Couldn't find field " <> G.unName fieldName <> " in root field " <> G.unName rootFieldName]
|
|
Just fieldType -> lookupDefinition rs fieldType f
|
|
|
|
lookupDefinition ::
|
|
RemoteSchemaIntrospection ->
|
|
GType ->
|
|
G.Field frag var ->
|
|
Either (FieldDef, Maybe (FieldAnalysis var)) [Text]
|
|
lookupDefinition rs t f = do
|
|
typeToSchemaM t \n ->
|
|
case lookupRS rs n of
|
|
Nothing -> Right ["Cannot find type definition for " <> G.unName n <> " in field " <> G.unName (G._fName f)]
|
|
Just tDef -> Left $ getDefinition rs tDef (G._fSelectionSet f)
|
|
|
|
field :: G.Selection frag var -> Maybe (G.Field frag var)
|
|
field (G.SelectionField f) = Just f
|
|
field _ = Nothing
|
|
|
|
infixl 7 &&&&
|
|
|
|
(&&&&) :: Applicative f => (t -> f a1) -> (t -> f a2) -> t -> f (a1, a2)
|
|
f &&&& g = \a -> (,) <$> f a <*> g a
|
|
|
|
getRootFieldNameFromOpType :: G.OperationType -> G.Name
|
|
getRootFieldNameFromOpType G.OperationTypeQuery = $$(G.litName "query_root")
|
|
getRootFieldNameFromOpType G.OperationTypeMutation = $$(G.litName "mutation_root")
|
|
getRootFieldNameFromOpType G.OperationTypeSubscription = $$(G.litName "subscription_root")
|
|
|
|
asObjectTypeDefinition :: G.TypeDefinition possibleTypes inputType -> Maybe (G.ObjectTypeDefinition inputType)
|
|
asObjectTypeDefinition (G.TypeDefinitionObject o) = Just o
|
|
asObjectTypeDefinition _ = Nothing
|
|
|
|
lookupRS :: RemoteSchemaIntrospection -> G.Name -> Maybe (TypeDefinition [Name] RemoteSchemaInputValueDefinition)
|
|
lookupRS (RemoteSchemaIntrospection rsDefs) n = Map.lookup n rsDefs
|
|
|
|
typeToSchemaM ::
|
|
GType ->
|
|
(Name -> Either (TypeDefinition [Name] RemoteSchemaInputValueDefinition, Maybe (FieldAnalysis var)) [Text]) ->
|
|
Either (FieldDef, Maybe (FieldAnalysis var)) [Text]
|
|
typeToSchemaM (G.TypeNamed n tName) k = case k tName of
|
|
Right x -> Right x
|
|
Left (t, x) -> Left (FieldInfo n t, x)
|
|
typeToSchemaM (G.TypeList n t) k = case typeToSchemaM t k of
|
|
Right x -> Right x
|
|
Left (t', x) -> Left (FieldList n t', x)
|
|
|
|
getDefinition ::
|
|
RemoteSchemaIntrospection ->
|
|
TypeDefinition [Name] RemoteSchemaInputValueDefinition ->
|
|
G.SelectionSet frag var ->
|
|
(TypeDefinition [Name] RemoteSchemaInputValueDefinition, Maybe (FieldAnalysis var))
|
|
getDefinition rs td sels =
|
|
(td,) $ case td of
|
|
(G.TypeDefinitionObject otd) -> do
|
|
ps <- traverse (\sel -> fmap (mkFieldAnalysis rs sel) (lookupFieldBySelection sel (G._otdFieldsDefinition otd))) sels
|
|
pure $ fold $ catMaybes ps
|
|
_ -> Nothing
|
|
|
|
itrListWith :: GType -> (Name -> p) -> p
|
|
itrListWith (G.TypeNamed _ tName) k = k tName
|
|
itrListWith (G.TypeList _ t) k = itrListWith t k
|
|
|
|
getFieldVars :: G.FieldDefinition RemoteSchemaInputValueDefinition -> HashMap Name (G.Value var) -> [(VarName, (GType, Maybe (G.Value var)))]
|
|
getFieldVars fDef' agMap =
|
|
map
|
|
( \RemoteSchemaInputValueDefinition {..} ->
|
|
(G._ivdName _rsitdDefinition, (G._ivdType _rsitdDefinition, Map.lookup (G._ivdName _rsitdDefinition) agMap))
|
|
)
|
|
(G._fldArgumentsDefinition fDef')
|
|
|
|
mkFieldAnalysis ::
|
|
RemoteSchemaIntrospection ->
|
|
G.Selection frag var ->
|
|
G.FieldDefinition RemoteSchemaInputValueDefinition ->
|
|
Maybe (FieldAnalysis var)
|
|
-- TODO: Handle `SelectionFragmentSpread` and `SelectionInlineFragment` as well
|
|
mkFieldAnalysis _ (G.SelectionFragmentSpread _) _ = Nothing
|
|
mkFieldAnalysis _ (G.SelectionInlineFragment _) _ = Nothing
|
|
mkFieldAnalysis rs (G.SelectionField f) fd = do
|
|
let ag = G._fArguments f
|
|
ft = G._fldType fd
|
|
fn = getFieldName f
|
|
(fFds, fVrs, fErrs) = itrListWith ft \n ->
|
|
case lookupRS rs n of
|
|
Nothing -> (mempty, mempty, ["Couldn't find definition for type " <> G.unName n <> " in field " <> G.unName fn <> " selected by " <> G.unName (G._fName f)])
|
|
Just tDef ->
|
|
let (def, fAn) = getDefinition rs tDef (G._fSelectionSet f)
|
|
in case ft of
|
|
G.TypeNamed n' _ ->
|
|
(OMap.singleton fn (FieldInfo n' def, fAn), Map.fromList (getFieldVars fd ag), [])
|
|
G.TypeList n' _ ->
|
|
(OMap.singleton fn (FieldList n' $ FieldInfo n' def, fAn), Map.fromList (getFieldVars fd ag), [])
|
|
|
|
pure
|
|
FieldAnalysis
|
|
{ _fFields = fFds,
|
|
_fVars = fVrs,
|
|
_fErrs = fErrs
|
|
}
|
|
|
|
lookupFieldBySelection :: G.Selection frag0 var0 -> [G.FieldDefinition RemoteSchemaInputValueDefinition] -> Maybe (G.FieldDefinition RemoteSchemaInputValueDefinition)
|
|
lookupFieldBySelection (G.SelectionField f) = find \d -> G._fName f == G._fldName d
|
|
lookupFieldBySelection _ = const Nothing
|