support union and interface types in remote schema (close #1276) (#1361)

This commit is contained in:
nizar-m
2019-01-28 22:15:10 +05:30
committed by Shahidh K Muhammed
parent 39bc3acffd
commit 32387ba964
25 changed files with 1063 additions and 50 deletions

View File

@@ -245,7 +245,7 @@ const analyzeFetcher = (url, headers, analyzeApiChange) => {
// Check if x-hasura-role is available in some form in the headers
const totalHeaders = Object.keys(reqHeaders);
totalHeaders.forEach((t) => {
totalHeaders.forEach(t => {
// If header has x-hasura-*
const lHead = t.toLowerCase();
if (lHead.slice(0, 'x-hasura-'.length) === 'x-hasura-') {

View File

@@ -38,9 +38,11 @@ const migrationNameTip = (
'run_sql_migration'
</Tooltip>
);
const trackTableTip = (hasFunctionSupport) => (
const trackTableTip = hasFunctionSupport => (
<Tooltip id="tooltip-tracktable">
{ `If you are creating a table/view${hasFunctionSupport ? '/function' : ''}, you can track them to query them
{`If you are creating a table/view${
hasFunctionSupport ? '/function' : ''
}, you can track them to query them
with GraphQL`}
</Tooltip>
);
@@ -299,7 +301,10 @@ const RawSQL = ({
data-test="raw-sql-track-check"
/>
Track {placeholderText}
<OverlayTrigger placement="right" overlay={trackTableTip(!!functionText)}>
<OverlayTrigger
placement="right"
overlay={trackTableTip(!!functionText)}
>
<i
className={`${styles.padd_small_left} fa fa-info-circle`}
aria-hidden="true"

View File

@@ -121,14 +121,9 @@ Current limitations
- Nodes from different GraphQL servers cannot be used in the same query/mutation. All top-level fields have to be
from the same GraphQL server.
- Subscriptions on remote GraphQL servers are not supported.
- Interfaces_ and Unions_ are not supported - if a remote schema has interfaces/unions, an error will be thrown if
you try to merge it.
These limitations will be addressed in upcoming versions.
.. _Interfaces: https://graphql.github.io/learn/schema/#interfaces
.. _Unions: https://graphql.github.io/learn/schema/#union-types
Extending the auto-generated GraphQL schema fields
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -133,10 +133,11 @@ mkHsraObjFldInfo descM name params ty =
mkHsraObjTyInfo
:: Maybe G.Description
-> G.NamedType
-> IFacesSet
-> ObjFieldMap
-> ObjTyInfo
mkHsraObjTyInfo descM ty flds =
mkObjTyInfo descM ty flds HasuraType
mkHsraObjTyInfo descM ty implIFaces flds =
mkObjTyInfo descM ty implIFaces flds HasuraType
mkHsraInpTyInfo
:: Maybe G.Description
@@ -318,7 +319,7 @@ defaultTypes = $(fromSchemaDocQ defaultSchema HasuraType)
mkGCtx :: TyAgg -> RootFlds -> InsCtxMap -> GCtx
mkGCtx (TyAgg tyInfos fldInfos ordByEnums funcArgCtx) (RootFlds flds) insCtxMap =
let queryRoot = mkHsraObjTyInfo (Just "query root")
(G.NamedType "query_root") $
(G.NamedType "query_root") Set.empty $
mapFromL _fiName (schemaFld:typeFld:qFlds)
scalarTys = map (TIScalar . mkHsraScalarTyInfo) colTys
compTys = map (TIInpObj . mkCompExpInp) colTys
@@ -338,12 +339,12 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums funcArgCtx) (RootFlds flds) insCtxMap
colTys = Set.toList $ Set.fromList $ map pgiType $
lefts $ Map.elems fldInfos
mkMutRoot =
mkHsraObjTyInfo (Just "mutation root") (G.NamedType "mutation_root") .
mkHsraObjTyInfo (Just "mutation root") (G.NamedType "mutation_root") Set.empty .
mapFromL _fiName
mutRootM = bool (Just $ mkMutRoot mFlds) Nothing $ null mFlds
mkSubRoot =
mkHsraObjTyInfo (Just "subscription root")
(G.NamedType "subscription_root") . mapFromL _fiName
(G.NamedType "subscription_root") Set.empty . mapFromL _fiName
subRootM = bool (Just $ mkSubRoot qFlds) Nothing $ null qFlds
(qFlds, mFlds) = partitionEithers $ map snd $ Map.elems flds
schemaFld = mkHsraObjFldInfo Nothing "__schema" Map.empty $

View File

@@ -11,6 +11,7 @@ import qualified Data.Aeson as J
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Language.GraphQL.Draft.Syntax as G
@@ -49,12 +50,11 @@ fetchRemoteSchema manager name def@(RemoteSchemaInfo url headerConf _) = do
introspectRes :: (FromIntrospection IntrospectionResult) <-
either schemaErr return $ J.eitherDecode respData
let (G.SchemaDocument tyDefs, qRootN, mRootN, sRootN) =
let (sDoc, qRootN, mRootN, sRootN) =
fromIntrospection introspectRes
let etTypeInfos = mapM fromRemoteTyDef tyDefs
typeInfos <- either schemaErr return etTypeInfos
let typMap = VT.mkTyInfoMap typeInfos
mQrTyp = Map.lookup qRootN typMap
typMap <- either remoteSchemaErr return $ VT.fromSchemaDoc sDoc $
VT.RemoteType name def
let mQrTyp = Map.lookup qRootN typMap
mMrTyp = maybe Nothing (\mr -> Map.lookup mr typMap) mRootN
mSrTyp = maybe Nothing (\sr -> Map.lookup sr typMap) sRootN
qrTyp <- liftMaybe noQueryRoot mQrTyp
@@ -66,8 +66,10 @@ fetchRemoteSchema manager name def@(RemoteSchemaInfo url headerConf _) = do
where
noQueryRoot = err400 Unexpected "query root not found in remote schema"
fromRemoteTyDef ty = VT.fromTyDef ty $ VT.RemoteType name def
schemaErr err = throw400 RemoteSchemaError (T.pack $ show err)
remoteSchemaErr :: (MonadError QErr m) => T.Text -> m a
remoteSchemaErr = throw400 RemoteSchemaError
schemaErr err = remoteSchemaErr (T.pack $ show err)
throwHttpErr :: (MonadError QErr m) => HTTP.HttpException -> m a
throwHttpErr = schemaErr
@@ -150,11 +152,11 @@ mergeMutRoot a b =
mkNewEmptyMutRoot :: VT.ObjTyInfo
mkNewEmptyMutRoot = VT.ObjTyInfo (Just "mutation root")
(G.NamedType "mutation_root") Map.empty
(G.NamedType "mutation_root") Set.empty Map.empty
mkNewMutRoot :: VT.ObjFieldMap -> VT.ObjTyInfo
mkNewMutRoot flds = VT.ObjTyInfo (Just "mutation root")
(G.NamedType "mutation_root") flds
(G.NamedType "mutation_root") Set.empty flds
mergeSubRoot :: GS.GCtx -> GS.GCtx -> Maybe VT.ObjTyInfo
mergeSubRoot a b =
@@ -171,7 +173,7 @@ mergeSubRoot a b =
mkNewEmptySubRoot :: VT.ObjTyInfo
mkNewEmptySubRoot = VT.ObjTyInfo (Just "subscription root")
(G.NamedType "subscription_root") Map.empty
(G.NamedType "subscription_root") Set.empty Map.empty
mergeTyMaps

View File

@@ -8,6 +8,7 @@ import Hasura.Prelude
import qualified Data.Aeson as J
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
@@ -75,14 +76,14 @@ objectTypeR
=> ObjTyInfo
-> Field
-> m J.Object
objectTypeR (ObjTyInfo descM n flds) fld =
objectTypeR (ObjTyInfo descM n iFaces flds) fld =
withSubFields (_fSelSet fld) $ \subFld ->
case _fName subFld of
"__typename" -> retJT "__Type"
"kind" -> retJ TKOBJECT
"name" -> retJ $ namedTyToTxt n
"description" -> retJ $ fmap G.unDescription descM
"interfaces" -> retJ ([] :: [()])
"interfaces" -> fmap J.toJSON $ mapM (`ifaceR` subFld) $ Set.toList iFaces
"fields" -> fmap J.toJSON $ mapM (`fieldR` subFld) $
sortBy (comparing _fiName) $
filter notBuiltinFld $ Map.elems flds
@@ -94,6 +95,55 @@ notBuiltinFld f =
where
fldName = _fiName f
getImplTypes :: (MonadReader t m, Has TypeMap t) => AsObjType -> m [ObjTyInfo]
getImplTypes aot = do
tyInfo :: TypeMap <- asks getter
return $ sortBy (comparing _otiName) $ Map.elems $ getPossibleObjTypes' tyInfo $ aot
-- 4.5.2.3
unionR :: (MonadReader t m, MonadError QErr m, Has TypeMap t) => UnionTyInfo -> Field -> m J.Object
unionR u@(UnionTyInfo descM n _) fld =
withSubFields (_fSelSet fld) $ \subFld ->
case _fName subFld of
"__typename" -> retJT "__Field"
"kind" -> retJ TKUNION
"name" -> retJ $ namedTyToTxt n
"description" -> retJ $ fmap G.unDescription descM
"possibleTypes" -> fmap J.toJSON $ mapM (`objectTypeR` subFld) =<< getImplTypes (AOTUnion u)
_ -> return J.Null
-- 4.5.2.4
ifaceR
:: ( MonadReader r m, Has TypeMap r
, MonadError QErr m)
=> G.NamedType
-> Field
-> m J.Object
ifaceR n fld = do
tyInfo <- getTyInfo n
case tyInfo of
TIIFace ifaceTyInfo -> ifaceR' ifaceTyInfo fld
_ -> throw500 $ "Unknown interface " <> G.unName (G.unNamedType n)
ifaceR'
:: ( MonadReader r m, Has TypeMap r
, MonadError QErr m)
=> IFaceTyInfo
-> Field
-> m J.Object
ifaceR' i@(IFaceTyInfo descM n flds) fld =
withSubFields (_fSelSet fld) $ \subFld ->
case _fName subFld of
"__typename" -> retJT "__Type"
"kind" -> retJ TKINTERFACE
"name" -> retJ $ namedTyToTxt n
"description" -> retJ $ fmap G.unDescription descM
"fields" -> fmap J.toJSON $ mapM (`fieldR` subFld) $
sortBy (comparing _fiName) $
filter notBuiltinFld $ Map.elems flds
"possibleTypes" -> fmap J.toJSON $ mapM (`objectTypeR` subFld) =<< getImplTypes (AOTIFace i)
_ -> return J.Null
-- 4.5.2.5
enumTypeR
:: ( Monad m )
@@ -179,6 +229,8 @@ namedTypeR' fld = \case
TIObj objTyInfo -> objectTypeR objTyInfo fld
TIEnum enumTypeInfo -> enumTypeR enumTypeInfo fld
TIInpObj inpObjTyInfo -> inputObjR inpObjTyInfo fld
TIIFace iFaceTyInfo -> ifaceR' iFaceTyInfo fld
TIUnion unionTyInfo -> unionR unionTyInfo fld
-- 4.5.3
fieldR

View File

@@ -417,7 +417,7 @@ mkTableObj
-> [SelField]
-> ObjTyInfo
mkTableObj tn allowedFlds =
mkObjTyInfo (Just desc) (mkTableTy tn) (mapFromL _fiName flds) HasuraType
mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) HasuraType
where
flds = concatMap (either (pure . mkPGColFld) mkRelFld') allowedFlds
mkRelFld' (relInfo, allowAgg, _, _, isNullable) =
@@ -433,7 +433,7 @@ type table_aggregate {
mkTableAggObj
:: QualifiedTable -> ObjTyInfo
mkTableAggObj tn =
mkHsraObjTyInfo (Just desc) (mkTableAggTy tn) $ mapFromL _fiName
mkHsraObjTyInfo (Just desc) (mkTableAggTy tn) Set.empty $ mapFromL _fiName
[aggFld, nodesFld]
where
desc = G.Description $
@@ -460,7 +460,7 @@ type table_aggregate_fields{
mkTableAggFldsObj
:: QualifiedTable -> [PGCol] -> [PGCol] -> ObjTyInfo
mkTableAggFldsObj tn numCols compCols =
mkHsraObjTyInfo (Just desc) (mkTableAggFldsTy tn) $ mapFromL _fiName $
mkHsraObjTyInfo (Just desc) (mkTableAggFldsTy tn) Set.empty $ mapFromL _fiName $
countFld : (numFlds <> compFlds)
where
desc = G.Description $
@@ -496,7 +496,7 @@ mkTableColAggFldsObj
-> [PGColInfo]
-> ObjTyInfo
mkTableColAggFldsObj tn op f cols =
mkHsraObjTyInfo (Just desc) (mkTableColAggFldsTy op tn) $ mapFromL _fiName $
mkHsraObjTyInfo (Just desc) (mkTableColAggFldsTy op tn) Set.empty $ mapFromL _fiName $
map mkColObjFld cols
where
desc = G.Description $ "aggregate " <> G.unName op <> " on columns"
@@ -649,7 +649,7 @@ mkMutRespObj
-> Bool -- is sel perm defined
-> ObjTyInfo
mkMutRespObj tn sel =
mkHsraObjTyInfo (Just objDesc) (mkMutRespTy tn) $ mapFromL _fiName
mkHsraObjTyInfo (Just objDesc) (mkMutRespTy tn) Set.empty $ mapFromL _fiName
$ affectedRowsFld : bool [] [returningFld] sel
where
objDesc = G.Description $

View File

@@ -237,8 +237,11 @@ validateNamedTypeVal inpValParser nt val = do
case tyInfo of
-- this should never happen
TIObj _ ->
throw500 $ "unexpected object type info for: "
<> showNamedTy nt
throwUnexpTypeErr "object"
TIIFace _ ->
throwUnexpTypeErr "interface"
TIUnion _ ->
throwUnexpTypeErr "union"
TIInpObj ioti ->
withParsed (getObject inpValParser) val $
fmap (AGObject nt) . mapM (validateObject inpValParser ioti)
@@ -249,6 +252,8 @@ validateNamedTypeVal inpValParser nt val = do
withParsed (getScalar inpValParser) val $
fmap (AGScalar pgColTy) . mapM (validateScalar pgColTy)
where
throwUnexpTypeErr ty = throw500 $ "unexpected " <> ty <> " type info for: "
<> showNamedTy nt
validateEnum enumTyInfo enumVal =
if Map.member enumVal (_etiValues enumTyInfo)
then return enumVal

View File

@@ -5,6 +5,9 @@ module Hasura.GraphQL.Validate.Types
, ObjFieldMap
, ObjTyInfo(..)
, mkObjTyInfo
, IFaceTyInfo(..)
, IFacesSet
, UnionTyInfo(..)
, FragDef(..)
, FragDefMap
, AnnVarVals
@@ -14,12 +17,16 @@ module Hasura.GraphQL.Validate.Types
, InpObjTyInfo(..)
, ScalarTyInfo(..)
, DirectiveInfo(..)
, AsObjType(..)
, defaultDirectives
, defDirectivesMap
, defaultSchema
, TypeInfo(..)
, isObjTy
, isIFaceTy
, getPossibleObjTypes'
, getObjTyM
, getUnionTyM
, mkScalarTy
, pgColTyToScalar
, pgColValToAnnGVal
@@ -27,6 +34,7 @@ module Hasura.GraphQL.Validate.Types
, mkTyInfoMap
, fromTyDef
, fromTyDefQ
, fromSchemaDoc
, fromSchemaDocQ
, TypeMap
, TypeLoc (..)
@@ -45,6 +53,7 @@ import Instances.TH.Lift ()
import qualified Data.Aeson 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
import qualified Language.GraphQL.Draft.TH as G
@@ -56,7 +65,6 @@ import Hasura.RQL.Types.RemoteSchema
import Hasura.SQL.Types
import Hasura.SQL.Value
-- | Typeclass for equating relevant properties of various GraphQL types
-- | defined below
class EquatableGType a where
@@ -143,30 +151,39 @@ fromFldDef (G.FieldDefinition descM n args ty _) loc =
type ObjFieldMap = Map.HashMap G.Name ObjFldInfo
type IFacesSet = Set.HashSet G.NamedType
data ObjTyInfo
= ObjTyInfo
{ _otiDesc :: !(Maybe G.Description)
, _otiName :: !G.NamedType
, _otiFields :: !ObjFieldMap
{ _otiDesc :: !(Maybe G.Description)
, _otiName :: !G.NamedType
, _otiImplIFaces :: !IFacesSet
, _otiFields :: !ObjFieldMap
} deriving (Show, Eq, TH.Lift)
instance EquatableGType ObjTyInfo where
type EqProps ObjTyInfo =
(G.NamedType, Map.HashMap G.Name (G.Name, G.GType, ParamMap))
getEqProps a = (,) (_otiName a) (Map.map getEqProps (_otiFields a))
(G.NamedType, Set.HashSet G.NamedType, Map.HashMap G.Name (G.Name, G.GType, ParamMap))
getEqProps a = (,,) (_otiName a) (_otiImplIFaces a) (Map.map getEqProps (_otiFields a))
instance Monoid ObjTyInfo where
mempty = ObjTyInfo Nothing (G.NamedType "") Map.empty
mempty = ObjTyInfo Nothing (G.NamedType "") Set.empty Map.empty
instance Semigroup ObjTyInfo where
objA <> objB =
objA { _otiFields = Map.union (_otiFields objA) (_otiFields objB)
, _otiImplIFaces = _otiImplIFaces objA `Set.union` _otiImplIFaces objB
}
mkObjTyInfo
:: Maybe G.Description -> G.NamedType -> ObjFieldMap -> TypeLoc -> ObjTyInfo
mkObjTyInfo descM ty flds loc =
ObjTyInfo descM ty $ Map.insert (_fiName newFld) newFld flds
:: 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
mkIFaceTyInfo :: Maybe G.Description -> G.NamedType -> Map.HashMap G.Name ObjFldInfo -> TypeLoc -> IFaceTyInfo
mkIFaceTyInfo descM ty flds loc =
IFaceTyInfo descM ty $ Map.insert (_fiName newFld) newFld flds
where newFld = typenameFld loc
typenameFld :: TypeLoc -> ObjFldInfo
@@ -177,11 +194,62 @@ typenameFld loc =
desc = "The name of the current Object type at runtime"
fromObjTyDef :: G.ObjectTypeDefinition -> TypeLoc -> ObjTyInfo
fromObjTyDef (G.ObjectTypeDefinition descM n _ _ flds) loc =
mkObjTyInfo descM (G.NamedType n) fldMap loc
fromObjTyDef (G.ObjectTypeDefinition descM n ifaces _ flds) loc =
mkObjTyInfo descM (G.NamedType n) (Set.fromList ifaces) fldMap loc
where
fldMap = Map.fromList [(G._fldName fld, fromFldDef fld loc) | fld <- flds]
data IFaceTyInfo
= IFaceTyInfo
{ _ifDesc :: !(Maybe G.Description)
, _ifName :: !G.NamedType
, _ifFields :: !ObjFieldMap
} deriving (Show, Eq, TH.Lift)
instance EquatableGType IFaceTyInfo where
type EqProps IFaceTyInfo =
(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
where
fldMap = Map.fromList [(G._fldName fld, fromFldDef fld loc) | fld <- flds]
type MemberTypes = Set.HashSet G.NamedType
data UnionTyInfo
= UnionTyInfo
{ _utiDesc :: !(Maybe G.Description)
, _utiName :: !(G.NamedType)
, _utiMemberTypes :: !MemberTypes
} deriving (Show, Eq, TH.Lift)
instance EquatableGType UnionTyInfo where
type EqProps UnionTyInfo =
(G.NamedType, Set.HashSet G.NamedType)
getEqProps a = (,) (_utiName a) (_utiMemberTypes a)
instance Monoid UnionTyInfo where
mempty = UnionTyInfo Nothing (G.NamedType "") Set.empty
instance Semigroup UnionTyInfo where
objA <> objB =
objA { _utiMemberTypes = Set.union (_utiMemberTypes objA) (_utiMemberTypes objB)
}
fromUnionTyDef :: G.UnionTypeDefinition -> UnionTyInfo
fromUnionTyDef (G.UnionTypeDefinition descM n _ mt) = UnionTyInfo descM (G.NamedType n) $ Set.fromList mt
type InpObjFldMap = Map.HashMap G.Name InpValInfo
data InpObjTyInfo
@@ -233,8 +301,29 @@ data TypeInfo
| TIObj !ObjTyInfo
| TIEnum !EnumTyInfo
| TIInpObj !InpObjTyInfo
| TIIFace !IFaceTyInfo
| TIUnion !UnionTyInfo
deriving (Show, Eq, TH.Lift)
data AsObjType
= AOTObj ObjTyInfo
| 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
toObjMap :: [ObjTyInfo] -> Map.HashMap G.NamedType ObjTyInfo
toObjMap objs = foldr (\o -> Map.insert (_otiName o) o) Map.empty objs
isObjTy :: TypeInfo -> Bool
isObjTy = \case
(TIObj _) -> True
@@ -245,6 +334,164 @@ getObjTyM = \case
(TIObj t) -> return t
_ -> Nothing
getUnionTyM :: TypeInfo -> Maybe UnionTyInfo
getUnionTyM = \case
(TIUnion u) -> return u
_ -> Nothing
isIFaceTy :: TypeInfo -> Bool
isIFaceTy = \case
(TIIFace _) -> True
_ -> False
data SchemaPath
= SchemaPath
{ _spTypeName :: !(Maybe G.NamedType)
, _spFldName :: !(Maybe G.Name)
, _spArgName :: !(Maybe G.Name)
, _spType :: !(Maybe T.Text)
}
setFldNameSP :: SchemaPath -> G.Name -> SchemaPath
setFldNameSP sp fn = sp { _spFldName = Just fn}
setArgNameSP :: SchemaPath -> G.Name -> SchemaPath
setArgNameSP sp an = sp { _spArgName = Just an}
showSP :: SchemaPath -> Text
showSP (SchemaPath t f a _) = maybe "" (\x -> showNamedTy x <> fN) t
where
fN = maybe "" (\x -> "." <> showName x <> aN) f
aN = maybe "" showArg a
showArg x = "(" <> showName x <> ":)"
showSPTxt' :: SchemaPath -> Text
showSPTxt' (SchemaPath _ f a t) = maybe "" (<> " "<> fld) t
where
fld = maybe "" (const $ "field " <> arg) f
arg = maybe "" (const "argument ") a
showSPTxt :: SchemaPath -> Text
showSPTxt p = showSPTxt' p <> showSP p
validateIFace :: MonadError Text f => IFaceTyInfo -> f ()
validateIFace (IFaceTyInfo _ n flds) = do
when (isFldListEmpty flds) $ throwError $ "List of fields cannot be empty for interface " <> showNamedTy n
validateObj :: TypeMap -> ObjTyInfo -> Either Text ()
validateObj tyMap objTyInfo@(ObjTyInfo _ n _ flds) = do
when (isFldListEmpty flds) $ throwError $ "List of fields cannot be empty for " <> objTxt
mapM_ (extrIFaceTyInfo' >=> validateIFaceImpl objTyInfo) $ _otiImplIFaces objTyInfo
where
extrIFaceTyInfo' t = withObjTxt $ extrIFaceTyInfo tyMap t
withObjTxt x = x `catchError` \e -> throwError $ e <> " implemented by " <> objTxt
objTxt = "Object type " <> showNamedTy n
validateIFaceImpl = implmntsIFace tyMap
isFldListEmpty :: ObjFieldMap -> Bool
isFldListEmpty = Map.null . Map.delete "__typename"
validateUnion :: MonadError Text m => TypeMap -> UnionTyInfo -> m ()
validateUnion tyMap (UnionTyInfo _ un mt) = do
when (Set.null mt) $ throwError $ "List of member types cannot be empty for union type " <> showNamedTy un
mapM_ valIsObjTy $ Set.toList mt
where
valIsObjTy mn = case Map.lookup mn tyMap of
Just (TIObj t) -> return t
Nothing -> throwError $ "Could not find type " <> showNamedTy mn <> ", which is defined as a member type of Union " <> showNamedTy un
_ -> throwError $ "Union type " <> showNamedTy un <> " can only include object types. It cannot include " <> showNamedTy mn
implmntsIFace :: TypeMap -> ObjTyInfo -> IFaceTyInfo -> Either Text ()
implmntsIFace tyMap objTyInfo iFaceTyInfo = do
let path =
( SchemaPath (Just $ _otiName objTyInfo) Nothing Nothing (Just "Object")
, SchemaPath (Just $ _ifName iFaceTyInfo) Nothing Nothing (Just "Interface")
)
mapM_ (includesIFaceFld path) $ _ifFields iFaceTyInfo
where
includesIFaceFld (spO,spIF) ifFld = do
let pathA@(spOA, spIFA) = (spO, setFldNameSP spIF $ _fiName ifFld)
objFld <- sameNameFld pathA ifFld
let pathB = (setFldNameSP spOA $ _fiName objFld, spIFA)
validateIsSubType' pathB (_fiTy objFld) (_fiTy ifFld)
hasAllArgs pathB objFld ifFld
isExtraArgsNullable pathB objFld ifFld
validateIsSubType' (spO,spIF) oFld iFld = validateIsSubType tyMap oFld iFld `catchError` \_ ->
throwError $ "The type of " <> showSPTxt spO <> " (" <> G.showGT oFld <>
") is not the same type/sub type of " <> showSPTxt spIF <> " (" <> G.showGT iFld <> ")"
sameNameFld (spO, spIF) ifFld = do
let spIFN = setFldNameSP spIF $ _fiName ifFld
onNothing (Map.lookup (_fiName ifFld) objFlds)
$ throwError $ showSPTxt spIFN <> " expected, but " <> showSP spO <> " does not provide it"
hasAllArgs (spO, spIF) objFld ifFld = forM_ (_fiParams ifFld) $ \ifArg -> do
objArg <- sameNameArg ifArg
let (spON, spIFN) = (setArgNameSP spO $ _iviName objArg, setArgNameSP spIF $ _iviName ifArg)
unless (_iviType objArg == _iviType ifArg) $ throwError $
showSPTxt spIFN <> " expects type " <> G.showGT (_iviType ifArg) <> ", but " <>
showSP spON <> " has type " <> G.showGT (_iviType objArg)
where
sameNameArg ivi = do
let spIFN = setArgNameSP spIF $ _iviName ivi
onNothing (Map.lookup (_iviName ivi) objArgs) $ throwError $ showSPTxt spIFN <> " required, but " <>
showSPTxt spO <> " does not provide it"
objArgs = _fiParams objFld
isExtraArgsNullable (spO, spIF) objFld ifFld = forM_ extraArgs isInpValNullable
where
extraArgs = Map.difference (_fiParams objFld) (_fiParams ifFld)
isInpValNullable ivi = unless (G.isNullable $ _iviType ivi) $ throwError $
showSPTxt (setArgNameSP spO $ _iviName ivi) <> " is of required type "
<> G.showGT (_iviType ivi) <> ", but is not provided by " <> showSPTxt spIF
objFlds = _otiFields objTyInfo
extrTyInfo :: TypeMap -> G.NamedType -> Either Text TypeInfo
extrTyInfo tyMap tn = maybe
(throwError $ "Could not find type with name " <> showNamedTy tn)
return
$ Map.lookup tn tyMap
extrIFaceTyInfo :: MonadError Text m => Map.HashMap G.NamedType TypeInfo -> G.NamedType -> m IFaceTyInfo
extrIFaceTyInfo tyMap tn = case Map.lookup tn tyMap of
Just (TIIFace i) -> return i
_ -> throwError $ "Could not find interface " <> showNamedTy tn
extrObjTyInfoM :: TypeMap -> G.NamedType -> Maybe ObjTyInfo
extrObjTyInfoM tyMap tn = case Map.lookup tn tyMap of
Just (TIObj o) -> return o
_ -> Nothing
validateIsSubType :: Map.HashMap G.NamedType TypeInfo -> G.GType -> G.GType -> Either Text ()
validateIsSubType tyMap subFldTy supFldTy = do
checkNullMismatch subFldTy supFldTy
case (subFldTy,supFldTy) of
(G.TypeNamed _ subTy, G.TypeNamed _ supTy) -> do
subTyInfo <- extrTyInfo tyMap subTy
supTyInfo <- extrTyInfo tyMap supTy
isSubTypeBase subTyInfo supTyInfo
(G.TypeList _ (G.ListType sub), G.TypeList _ (G.ListType sup) ) -> do
validateIsSubType tyMap sub sup
_ -> throwError $ showIsListTy subFldTy <> " Type " <> G.showGT subFldTy <>
" cannot be a sub-type of " <> showIsListTy supFldTy <> " Type " <> G.showGT supFldTy
where
checkNullMismatch subTy supTy = when (G.isNotNull supTy && G.isNullable subTy ) $
throwError $ "Nullable Type " <> G.showGT subFldTy <> " cannot be a sub-type of Non-Null Type " <> G.showGT supFldTy
showIsListTy = \case
G.TypeList {} -> "List"
G.TypeNamed {} -> "Named"
-- TODO Should we check the schema location as well?
isSubTypeBase :: (MonadError Text m) => TypeInfo -> TypeInfo -> m ()
isSubTypeBase subTyInfo supTyInfo = case (subTyInfo,supTyInfo) of
(TIObj obj, TIIFace iFace) -> unless (_ifName iFace `elem` _otiImplIFaces obj) notSubTyErr
_ -> unless (subTyInfo == supTyInfo) notSubTyErr
where
showTy = showNamedTy . getNamedTy
notSubTyErr = throwError $ "Type " <> showTy subTyInfo <> " is not a sub type of " <> showTy supTyInfo
-- map postgres types to builtin scalars
pgColTyToScalar :: PGColType -> Text
pgColTyToScalar = \case
@@ -263,8 +510,10 @@ getNamedTy :: TypeInfo -> G.NamedType
getNamedTy = \case
TIScalar t -> mkScalarTy $ _stiType t
TIObj t -> _otiName t
TIIFace i -> _ifName i
TIEnum t -> _etiName t
TIInpObj t -> _iotiName t
TIUnion u -> _utiName u
mkTyInfoMap :: [TypeInfo] -> TypeMap
mkTyInfoMap tyInfos =
@@ -275,20 +524,34 @@ fromTyDef tyDef loc = case tyDef of
G.TypeDefinitionScalar t -> TIScalar <$> fromScalarTyDef t loc
G.TypeDefinitionObject t -> return $ TIObj $ fromObjTyDef t loc
G.TypeDefinitionInterface t ->
throwError $ "unexpected interface: " <> showName (G._itdName t)
G.TypeDefinitionUnion t ->
throwError $ "unexpected union: " <> showName (G._utdName t)
return $ TIIFace $ fromIFaceDef t loc
G.TypeDefinitionUnion t -> return $ TIUnion $ fromUnionTyDef t
G.TypeDefinitionEnum t -> return $ TIEnum $ fromEnumTyDef t loc
G.TypeDefinitionInputObject t -> return $ TIInpObj $ fromInpObjTyDef t loc
fromSchemaDoc :: G.SchemaDocument -> TypeLoc -> Either Text TypeMap
fromSchemaDoc (G.SchemaDocument tyDefs) loc = do
tyMap <- fmap mkTyInfoMap $ mapM (flip fromTyDef loc) tyDefs
validateTypeMap tyMap
return tyMap
validateTypeMap :: TypeMap -> Either Text ()
validateTypeMap tyMap = mapM_ validateTy $ Map.elems tyMap
where
validateTy (TIObj o) = validateObj tyMap o
validateTy (TIUnion u) = validateUnion tyMap u
validateTy (TIIFace i) = validateIFace i
validateTy _ = return ()
fromTyDefQ :: G.TypeDefinition -> TypeLoc -> TH.Q TH.Exp
fromTyDefQ tyDef loc = case fromTyDef tyDef loc of
Left e -> fail $ T.unpack e
Right t -> TH.lift t
fromSchemaDocQ :: G.SchemaDocument -> TypeLoc -> TH.Q TH.Exp
fromSchemaDocQ (G.SchemaDocument tyDefs) loc =
TH.ListE <$> mapM (flip fromTyDefQ loc) tyDefs
fromSchemaDocQ sd loc = case fromSchemaDoc sd loc of
Left e -> fail $ T.unpack e
Right tyMap -> TH.ListE <$> mapM TH.lift (Map.elems tyMap)
defaultSchema :: G.SchemaDocument
defaultSchema = $(G.parseSchemaDocQ "src-rsr/schema.graphql")

View File

@@ -8,6 +8,10 @@ import Instances.TH.Lift ()
import qualified Language.Haskell.TH.Syntax as TH
import qualified Data.HashMap.Strict as M
import qualified Data.HashSet as S
instance (TH.Lift k, TH.Lift v) => TH.Lift (M.HashMap k v) where
lift m = [| M.fromList $(TH.lift $ M.toList m) |]
instance TH.Lift a => TH.Lift (S.HashSet a) where
lift s = [| S.fromList $(TH.lift $ S.toList s) |]

View File

@@ -4,6 +4,8 @@ from http import HTTPStatus
import graphene
import copy
from webserver import RequestHandler, WebServer, MkHandlers, Response
from enum import Enum
@@ -167,6 +169,344 @@ class PersonGraphQL(RequestHandler):
res = person_schema.execute(req.json['query'])
return mkJSONResp(res)
#GraphQL server with interfaces
class Character(graphene.Interface):
id = graphene.ID(required=True)
name = graphene.String(required=True)
def __init__(self, id, name):
self.id = id
self.name = name
class Human(graphene.ObjectType):
class Meta:
interfaces = (Character, )
home_planet = graphene.String()
def __init__(self, home_planet, character):
self.home_planet = home_planet
self.character = character
def resolve_id(self, info):
return self.character.id
def resolve_name(self, info):
return self.character.name
def refolve_primary_function(self, info):
return self.home_planet
class Droid(graphene.ObjectType):
class Meta:
interfaces = (Character, )
primary_function = graphene.String()
def __init__(self, primary_function, character):
self.primary_function = primary_function
self.character = character
def resolve_id(self, info):
return self.character.id
def resolve_name(self, info):
return self.character.name
def resolve_primary_function(self, info):
return self.primary_function
class CharacterSearchResult(graphene.Union):
class Meta:
types = (Human,Droid)
all_characters = {
4: Droid("Astromech", Character(1,'R2-D2')),
5: Human("Tatooine", Character(2, "Luke Skywalker")),
}
character_search_results = {
1: Droid("Astromech", Character(6,'R2-D2')),
2: Human("Tatooine", Character(7, "Luke Skywalker")),
}
class CharacterIFaceQuery(graphene.ObjectType):
hero = graphene.Field(
Character,
required=False,
episode=graphene.Int(required=True)
)
def resolve_hero(_, info, episode):
return all_characters.get(episode)
schema = graphene.Schema(query=CharacterIFaceQuery, types=[Human, Droid])
character_interface_schema = graphene.Schema(query=CharacterIFaceQuery, types=[Human, Droid])
class CharacterInterfaceGraphQL(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
return mkJSONResp(res)
class InterfaceGraphQLErrEmptyFieldList(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'INTERFACE':
t['fields'] = []
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class InterfaceGraphQLErrUnknownInterface(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'OBJECT' and t['name'] == 'Droid':
t['interfaces'][0]['name'] = 'UnknownIFace'
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class InterfaceGraphQLErrWrongFieldType(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
#Remove id field from Droid
if t['kind'] == 'OBJECT' and t['name'] == 'Droid':
for f in t['fields'].copy():
if f['name'] == 'id':
f['type']['ofType']['name'] = 'String'
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class InterfaceGraphQLErrMissingField(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
#Remove id field from Droid
if t['kind'] == 'OBJECT' and t['name'] == 'Droid':
for f in t['fields'].copy():
if f['name'] == 'id':
t['fields'].remove(f)
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
ifaceArg = {
"name": "ifaceArg",
"description": None,
"type": {
"kind": "NON_NULL",
"name": None,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": None
}
},
"defaultValue": None
}
class InterfaceGraphQLErrMissingArg(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'INTERFACE':
for f in t['fields']:
if f['name'] == 'id':
f['args'].append(ifaceArg)
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class InterfaceGraphQLErrWrongArgType(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
objArg = copy.deepcopy(ifaceArg)
objArg['type']['ofType']['name'] = 'String'
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in filter(lambda ty : ty['kind'] == 'INTERFACE', typesList):
for f in filter(lambda fld: fld['name'] == 'id', t['fields']):
f['args'].append(ifaceArg)
for t in filter(lambda ty: ty['name'] in ['Droid','Human'], typesList):
for f in filter(lambda fld: fld['name'] == 'id', t['fields']):
f['args'].append(ifaceArg if t['name'] == 'Droid' else objArg)
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class InterfaceGraphQLErrExtraNonNullArg(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = character_interface_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'OBJECT' and t['name'] == 'Droid':
for f in t['fields']:
if f['name'] == 'id':
f['args'].append({
"name": "extraArg",
"description": None,
"type": {
"kind": "NON_NULL",
"name": None,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": None
}
},
"defaultValue": None
})
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
#GraphQL server involving union type
class UnionQuery(graphene.ObjectType):
search = graphene.Field(
CharacterSearchResult,
required=False,
episode=graphene.Int(required=True)
)
def resolve_search(_, info, episode):
return character_search_results.get(episode)
union_schema = graphene.Schema(query=UnionQuery, types=[Human, Droid])
class UnionGraphQL(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = union_schema.execute(req.json['query'])
return mkJSONResp(res)
class UnionGraphQLSchemaErrUnknownTypes(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = union_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'UNION':
for i, p in enumerate(t['possibleTypes']):
p['name'] = 'Unknown' + str(i)
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class UnionGraphQLSchemaErrSubTypeInterface(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = union_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'UNION':
for p in t['possibleTypes']:
p['name'] = 'Character'
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class UnionGraphQLSchemaErrNoMemberTypes(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = union_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'UNION':
t['possibleTypes'] = []
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
class UnionGraphQLSchemaErrWrappedType(RequestHandler):
def get(self, req):
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
def post(self, req):
if not req.json:
return Response(HTTPStatus.BAD_REQUEST)
res = union_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
if typesList is not None:
for t in typesList:
if t['kind'] == 'UNION':
for i, p in enumerate(t['possibleTypes']):
t['possibleTypes'][i] = {
"kind": "NON_NULL",
"name": None,
"ofType": p
}
return Response(HTTPStatus.OK, respDict,
{'Content-Type': 'application/json'})
#GraphQL server with default values for inputTypes
class InpObjType(graphene.InputObjectType):
@classmethod
@@ -229,6 +569,7 @@ class EchoGraphQL(RequestHandler):
res = echo_schema.execute(req.json['query'])
respDict = res.to_dict()
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
#Hack around enum default_value serialization issue: https://github.com/graphql-python/graphql-core/issues/166
if typesList is not None:
for t in filter(lambda ty: ty['name'] == 'EchoQuery', typesList):
for f in filter(lambda fld: fld['name'] == 'echo', t['fields']):
@@ -242,6 +583,19 @@ handlers = MkHandlers({
'/hello-graphql': HelloGraphQL,
'/user-graphql': UserGraphQL,
'/country-graphql': CountryGraphQL,
'/character-iface-graphql' : CharacterInterfaceGraphQL,
'/iface-graphql-err-empty-field-list' : InterfaceGraphQLErrEmptyFieldList,
'/iface-graphql-err-unknown-iface' : InterfaceGraphQLErrUnknownInterface,
'/iface-graphql-err-missing-field' : InterfaceGraphQLErrMissingField,
'/iface-graphql-err-wrong-field-type' : InterfaceGraphQLErrWrongFieldType,
'/iface-graphql-err-missing-arg' : InterfaceGraphQLErrMissingArg,
'/iface-graphql-err-wrong-arg-type' : InterfaceGraphQLErrWrongArgType,
'/iface-graphql-err-extra-non-null-arg' : InterfaceGraphQLErrExtraNonNullArg,
'/union-graphql' : UnionGraphQL,
'/union-graphql-err-unknown-types' : UnionGraphQLSchemaErrUnknownTypes,
'/union-graphql-err-subtype-iface' : UnionGraphQLSchemaErrSubTypeInterface,
'/union-graphql-err-no-member-types' : UnionGraphQLSchemaErrNoMemberTypes,
'/union-graphql-err-wrapped-type' : UnionGraphQLSchemaErrWrappedType,
'/default-value-echo-graphql' : EchoGraphQL,
'/person-graphql': PersonGraphQL
})

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a field of an object not having the argument of the same field in interface
url: /v1/query
status: 400
response:
path: $.args
error: |-
Interface field argument 'Character'."id"("ifaceArg":) required, but Object field 'Droid'."id" does not provide it
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-missing-arg
definition:
url: http://localhost:5000/iface-graphql-err-missing-arg
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with an object not having a field defined in the interface that it implements
url: /v1/query
status: 400
response:
path: $.args
error: |-
Interface field 'Character'."id" expected, but 'Droid' does not provide it
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-missing-arg
definition:
url: http://localhost:5000/iface-graphql-err-missing-field
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with an object implementing unknown interface
url: /v1/query
status: 400
response:
path: $.args
error: |-
Could not find interface 'UnknownIFace' implemented by Object type 'Droid'
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-iface
definition:
url: http://localhost:5000/iface-graphql-err-unknown-iface
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a argument of a field of an object not having the same type as the same argument of the same field in interface
url: /v1/query
status: 400
response:
path: $.args
error: |-
Interface field argument 'Character'."id"("ifaceArg":) expects type Int!, but 'Human'."id"("ifaceArg":) has type String!
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-missing-arg
definition:
url: http://localhost:5000/iface-graphql-err-wrong-arg-type
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with an interface with field list empty
url: /v1/query
status: 400
response:
path: $.args
error: |-
List of fields cannot be empty for interface 'Character'
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-types
definition:
url: http://localhost:5000/iface-graphql-err-empty-field-list
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a field of object implementing the interface having an extra non-null arg
url: /v1/query
status: 400
response:
path: $.args
error: |-
Object field argument 'Droid'."id"("extraArg":) is of required type Int!, but is not provided by Interface field 'Character'."id"
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-types
definition:
url: http://localhost:5000/iface-graphql-err-extra-non-null-arg
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with an object implementing the interface has a field with a different type when compared to the same field in the interface
url: /v1/query
status: 400
response:
path: $.args
error: |-
The type of Object field 'Droid'."id" (String!) is not the same type/sub type of Interface field 'Character'."id" (ID!)
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-types
definition:
url: http://localhost:5000/iface-graphql-err-wrong-field-type
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a union which has unknown object types
url: /v1/query
status: 400
response:
path: $.args
error: |-
Union type 'CharacterSearchResult' can only include object types. It cannot include 'Character'
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-types
definition:
url: http://localhost:5000/union-graphql-err-subtype-iface
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a union which has unknown object types
url: /v1/query
status: 400
response:
path: $.args
error: |-
List of member types cannot be empty for union type 'CharacterSearchResult'
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-no-mem-types
definition:
url: http://localhost:5000/union-graphql-err-no-member-types
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a union which has unknown object types
url: /v1/query
status: 400
response:
path: $.args
error: |-
Could not find type 'Unknown0', which is defined as a member type of Union 'CharacterSearchResult'
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-types
definition:
url: http://localhost:5000/union-graphql-err-unknown-types
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,16 @@
description: Add a remote schema with a union which has wrapped type as a possible type
url: /v1/query
status: 400
response:
path: $.args
error: |-
"Error in $.types[1].possibleTypes[0].name: expected Text, encountered Null"
code: remote-schema-error
query:
type: add_remote_schema
args:
name: err-unknown-types
definition:
url: http://localhost:5000/union-graphql-err-wrapped-type
headers: []
forward_client_headers: true

View File

@@ -0,0 +1,43 @@
description: Query from the remote schema with interfaces
url: /v1alpha1/graphql
status: 200
response:
data:
hero3: null
hero4:
__typename: Droid
id: '1'
name: R2-D2
primaryFunction: Astromech
hero5:
__typename: Human
name: Luke Skywalker
homePlanet: Tatooine
query:
query: |
query getHeroes {
hero3: hero (episode: 3) {
id
}
hero4: hero (episode: 4) {
__typename
id
name
... on Droid {
primaryFunction
}
}
hero5: hero (episode: 5) {
__typename
name
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
}

View File

@@ -0,0 +1,51 @@
description: Query from the remote schema with union
url: /v1alpha1/graphql
status: 200
response:
data:
hero3: null
hero2:
__typename: Human
id: '7'
name: Luke Skywalker
homePlanet: Tatooine
hero1:
__typename: Droid
id: '6'
name: R2-D2
primaryFunction: Astromech
query:
query: |
query unionSearch {
hero3: search (episode: 3) {
__typename
... on Droid {
id
}
}
hero2: search (episode: 2) {
__typename
... on Human {
id
name
homePlanet
}
}
hero1: search (episode: 1) {
__typename
... on Droid {
id
name
primaryFunction
}
... on Human {
id
homePlanet
}
}
}

View File

@@ -84,6 +84,68 @@ class TestRemoteSchemaBasic:
hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "my remote"}})
assert st_code == 200, resp
def test_add_remote_schema_with_interfaces(self, hge_ctx):
"""add a remote schema with interfaces in it"""
q = mk_add_remote_q('my remote interface one', 'http://localhost:5000/character-iface-graphql')
st_code, resp = hge_ctx.v1q(q)
assert st_code == 200, resp
check_query_f(hge_ctx, self.dir + '/character_interface_query.yaml')
hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "my remote interface one"}})
assert st_code == 200, resp
def test_add_remote_schema_with_interface_err_empty_fields_list(self, hge_ctx):
"""add a remote schema with an interface having no fields"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_iface_err_empty_fields_list.yaml')
def test_add_remote_schema_err_unknown_interface(self, hge_ctx):
"""add a remote schema with an interface having no fields"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_err_unknown_interface.yaml')
def test_add_remote_schema_with_interface_err_missing_field(self, hge_ctx):
"""add a remote schema where an object implementing an interface does not have a field defined in the interface"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_err_missing_field.yaml')
def test_add_remote_schema_with_interface_err_wrong_field_type(self, hge_ctx):
"""add a remote schema where an object implementing an interface have a field with the same name as in the interface, but of different type"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_iface_err_wrong_field_type.yaml')
def test_add_remote_schema_with_interface_err_missing_arg(self, hge_ctx):
"""add a remote schema where a field of an object implementing an interface does not have the argument defined in the same field of interface"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_err_missing_arg.yaml')
def test_add_remote_schema_with_interface_err_wrong_arg_type(self, hge_ctx):
"""add a remote schema where the argument of a field of an object implementing the interface does not have the same type as the argument defined in the field of interface"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_iface_err_wrong_arg_type.yaml')
def test_add_remote_schema_with_interface_err_extra_non_null_arg(self, hge_ctx):
"""add a remote schema with a field of an object implementing interface having extra non_null argument"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_iface_err_extra_non_null_arg.yaml')
def test_add_remote_schema_with_union(self, hge_ctx):
"""add a remote schema with union in it"""
q = mk_add_remote_q('my remote union one', 'http://localhost:5000/union-graphql')
st_code, resp = hge_ctx.v1q(q)
assert st_code == 200, resp
check_query_f(hge_ctx, self.dir + '/search_union_type_query.yaml')
hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "my remote union one"}})
assert st_code == 200, resp
def test_add_remote_schema_with_union_err_no_member_types(self, hge_ctx):
"""add a remote schema with a union having no member types"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_union_err_no_member_types.yaml')
def test_add_remote_schema_with_union_err_unkown_types(self, hge_ctx):
"""add a remote schema with a union having unknown types as memberTypes"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_union_err_unknown_types.yaml')
def test_add_remote_schema_with_union_err_subtype_iface(self, hge_ctx):
"""add a remote schema with a union having interface as a memberType"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_union_err_member_type_interface.yaml')
def test_add_remote_schema_with_union_err_wrapped_type(self, hge_ctx):
"""add a remote schema with error in spec for union"""
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_union_err_wrapped_type.yaml')
def test_bulk_remove_add_remote_schema(self, hge_ctx):
st_code, resp = hge_ctx.v1q_f(self.dir + '/basic_bulk_remove_add.yaml')
assert st_code == 200, resp