mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 04:24:35 +03:00
2a9bc2354f
* validation support for unions and interfaces * refactor SQL generation logic for improved readability * '/v1/relay' endpoint for relay schema * implement 'Node' interface and top level 'node' field resolver * add relay toggle on graphiql * fix explain api response & index plan id with query type * add hasura mutations to relay * add relay pytests * update CHANGELOG.md Co-authored-by: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Co-authored-by: Rishichandra Wawhal <rishi@hasura.io> Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
301 lines
9.1 KiB
Haskell
301 lines
9.1 KiB
Haskell
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE TypeFamilyDependencies #-}
|
|
module Hasura.GraphQL.NormalForm
|
|
( Selection(..)
|
|
, NormalizedSelection
|
|
, NormalizedSelectionSet
|
|
, NormalizedField
|
|
, SelectionSet(..)
|
|
, RootSelectionSet(..)
|
|
-- , toGraphQLOperation
|
|
, ArgsMap
|
|
, Field(..)
|
|
, Typename(..)
|
|
, IsField(..)
|
|
, toField
|
|
, AliasedFields(..)
|
|
, asObjectSelectionSet
|
|
, ObjectSelectionSet(..)
|
|
, ObjectSelectionSetMap
|
|
, traverseObjectSelectionSet
|
|
, InterfaceSelectionSet
|
|
, asInterfaceSelectionSet
|
|
, getMemberSelectionSet
|
|
, UnionSelectionSet
|
|
, ScopedSelectionSet(..)
|
|
, emptyScopedSelectionSet
|
|
, getUnionSelectionSet
|
|
, getInterfaceSelectionSet
|
|
, getObjectSelectionSet
|
|
|
|
, AnnInpVal(..)
|
|
, AnnGValue(..)
|
|
, AnnGObject
|
|
, AnnGEnumValue(..)
|
|
, hasNullVal
|
|
, getAnnInpValKind
|
|
|
|
, toGraphQLField
|
|
, toGraphQLSelectionSet
|
|
) 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 qualified Hasura.RQL.Types.Column as RQL
|
|
import qualified Hasura.RQL.Types.Error as RQL
|
|
import Hasura.SQL.Types
|
|
import Hasura.SQL.Value
|
|
|
|
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 NormalizedSelectionSet a = s | s -> a
|
|
|
|
-- | What a processed G.Field should look like
|
|
type family NormalizedField a
|
|
|
|
type NormalizedSelection a
|
|
= Selection (NormalizedField a) (NormalizedSelectionSet 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 RootSelectionSet
|
|
= RQuery !ObjectSelectionSet
|
|
| RMutation !ObjectSelectionSet
|
|
| 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 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
|
|
= 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
|
|
|
|
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
|
|
= Field
|
|
{ _fName :: !G.Name
|
|
, _fType :: !G.NamedType
|
|
, _fArguments :: !ArgsMap
|
|
, _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
|
|
-- )
|
|
|
|
-- $(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"
|