这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions docs/graphql/manual/remote-schemas/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,22 @@ Use-cases
- Querying data that is not available in your database


You can handle these use-cases by writing resolvers in a custom GraphQL server and making Hasura merge this ``remote schema`` with the existing autogenerated schema. You can also add multiple remote schemas. Think of the merged schema as a union of top-level nodes from each of the sub-schemas.
You can handle these use-cases by writing resolvers in a custom GraphQL server
and making Hasura merge this ``remote schema`` with the existing autogenerated
schema. You can also add multiple remote schemas. Think of the merged schema as
a union of top-level nodes from each of the sub-schemas.

Note that if you are looking for adding authorization & access control for your
app users to the GraphQL APIs that are auto-generated via Hasura, head to
:doc:`Authorization / Access control <../auth/index>`

.. note::

**Nomenclature**:

Top-level node names need to be unique across all merged schemas (*case-sensitive match*).
Types with the *exact same name and structure* will be merged. But types with *same name but different structure* will result in type conflicts.

Note that if you are looking for adding authorization & access control for your app users
to the GraphQL APIs that are auto-generated via Hasura, head to :doc:`Authorization / Access control <../auth/index>`

How to add a remote schema
--------------------------
Expand All @@ -51,11 +63,11 @@ is to use one of our boilerplates:
- `Boilerplates <https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/graphql-servers>`__
- `Serverless boilerplates <https://github.com/hasura/graphql-serverless>`__


.. note::

**Current limitations**:

- Nomenclature: Type names and node names need to be unique across all merged schemas (*case-sensitive match*). In the next few iterations, support for merging types with the exact same name and structure will be available.
- Nodes from different GraphQL servers cannot be used in the same query/mutation. All top-level nodes have to be from the same GraphQL server.
- Subscriptions on remote GraphQL server 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.
Expand Down
2 changes: 1 addition & 1 deletion server/src-lib/Hasura/GraphQL/Context.hs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
] <>
scalarTys <> compTys <> defaultTypes
-- for now subscription root is query root
in GCtx allTys fldInfos ordByEnums queryRoot mutRootM (Just queryRoot)
in GCtx allTys fldInfos ordByEnums queryRoot mutRootM subRootM
(Map.map fst flds) insCtxMap
where
mkMutRoot =
Expand Down
28 changes: 26 additions & 2 deletions server/src-lib/Hasura/GraphQL/RemoteServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,20 @@ fetchRemoteSchema manager name def@(RemoteSchemaInfo url headerConf _) = do

introspectRes :: (FromIntrospection IntrospectionResult) <-
either schemaErr return $ J.eitherDecode respData
let (G.SchemaDocument tyDefs, qRootN, mRootN, _) =
let (G.SchemaDocument tyDefs, 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
mMrTyp = maybe Nothing (\mr -> Map.lookup mr typMap) mRootN
mSrTyp = maybe Nothing (\sr -> Map.lookup sr typMap) sRootN
qrTyp <- liftMaybe noQueryRoot mQrTyp
let mRmQR = VT.getObjTyM qrTyp
mRmMR = join $ VT.getObjTyM <$> mMrTyp
mRmSR = join $ VT.getObjTyM <$> mSrTyp
rmQR <- liftMaybe (err400 Unexpected "query root has to be an object type") mRmQR
return $ GS.RemoteGCtx typMap rmQR mRmMR Nothing
return $ GS.RemoteGCtx typMap rmQR mRmMR mRmSR

where
noQueryRoot = err400 Unexpected "query root not found in remote schema"
Expand Down Expand Up @@ -95,6 +97,7 @@ mkDefaultRemoteGCtx
mkDefaultRemoteGCtx =
foldlM (\combG -> mergeGCtx combG . convRemoteGCtx) GS.emptyGCtx

-- merge a remote schema `gCtx` into current `gCtxMap`
mergeRemoteSchema
:: (MonadError QErr m)
=> GS.GCtxMap
Expand All @@ -117,10 +120,12 @@ mergeGCtx gCtx rmMergedGCtx = do
GS.checkSchemaConflicts gCtx rmMergedGCtx
let newQR = mergeQueryRoot gCtx rmMergedGCtx
newMR = mergeMutRoot gCtx rmMergedGCtx
newSR = mergeSubRoot gCtx rmMergedGCtx
newTyMap = mergeTyMaps hsraTyMap rmTypes newQR newMR
updatedGCtx = gCtx { GS._gTypes = newTyMap
, GS._gQueryRoot = newQR
, GS._gMutRoot = newMR
, GS._gSubRoot = newSR
}
return updatedGCtx

Expand All @@ -129,6 +134,7 @@ convRemoteGCtx rmGCtx =
GS.emptyGCtx { GS._gTypes = GS._rgTypes rmGCtx
, GS._gQueryRoot = GS._rgQueryRoot rmGCtx
, GS._gMutRoot = GS._rgMutationRoot rmGCtx
, GS._gSubRoot = GS._rgSubscriptionRoot rmGCtx
}


Expand Down Expand Up @@ -156,6 +162,24 @@ mkNewMutRoot :: VT.ObjFieldMap -> VT.ObjTyInfo
mkNewMutRoot flds = VT.ObjTyInfo (Just "mutation root")
(G.NamedType "mutation_root") flds

mergeSubRoot :: GS.GCtx -> GS.GCtx -> Maybe VT.ObjTyInfo
mergeSubRoot a b =
let objA' = fromMaybe mempty $ GS._gSubRoot a
objB = fromMaybe mempty $ GS._gSubRoot b
objA = newRootOrEmpty objA' objB
merged = objA <> objB
in bool (Just merged) Nothing $ merged == mempty
where
newRootOrEmpty x y =
if x == mempty && y /= mempty
then mkNewEmptySubRoot
else x

mkNewEmptySubRoot :: VT.ObjTyInfo
mkNewEmptySubRoot = VT.ObjTyInfo (Just "subscription root")
(G.NamedType "subscription_root") Map.empty


mergeTyMaps
:: VT.TypeMap
-> VT.TypeMap
Expand Down
67 changes: 46 additions & 21 deletions server/src-lib/Hasura/GraphQL/Schema.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1582,19 +1582,28 @@ checkSchemaConflicts
:: (MonadError QErr m)
=> GCtx -> GCtx -> m ()
checkSchemaConflicts gCtx remoteCtx = do
-- check type conflicts
let typeMap = _gTypes gCtx -- hasura typemap
hTypes = map G.unNamedType $ Map.keys typeMap
-- check type conflicts
let hTypes = Map.elems typeMap
hTyNames = map G.unNamedType $ Map.keys typeMap
-- get the root names from the remote schema
rmQRootName = _otiName $ _gQueryRoot remoteCtx
rmMRootName = maybeToList $ _otiName <$> _gMutRoot remoteCtx
rmRootNames = map G.unNamedType (rmQRootName:rmMRootName)
rmTypes = filter (`notElem` builtinTy ++ rmRootNames) $
map G.unNamedType $ Map.keys $ _gTypes remoteCtx

conflictedTypes = filter (`elem` hTypes) rmTypes

unless (null conflictedTypes) $
throw400 RemoteSchemaConflicts $ tyMsg conflictedTypes
rmSRootName = maybeToList $ _otiName <$> _gSubRoot remoteCtx
rmRootNames = map G.unNamedType (rmQRootName:(rmMRootName ++ rmSRootName))
let rmTypes = Map.filterWithKey
(\k _ -> G.unNamedType k `notElem` builtinTy ++ rmRootNames)
$ _gTypes remoteCtx

isTyInfoSame ty = any (\t -> tyinfoEq t ty) hTypes
-- name is same and structure is not same
isSame n ty = G.unNamedType n `elem` hTyNames &&
not (isTyInfoSame ty)
conflictedTypes = Map.filterWithKey isSame rmTypes
conflictedTyNames = map G.unNamedType $ Map.keys conflictedTypes

unless (Map.null conflictedTypes) $
throw400 RemoteSchemaConflicts $ tyMsg conflictedTyNames

-- check node conflicts
let rmQRoot = _otiFields $ _gQueryRoot remoteCtx
Expand All @@ -1615,6 +1624,13 @@ checkSchemaConflicts gCtx remoteCtx = do
_ -> return ()

where
tyinfoEq a b = case (a, b) of
(TIScalar t1, TIScalar t2) -> typeEq t1 t2
(TIObj t1, TIObj t2) -> typeEq t1 t2
(TIEnum t1, TIEnum t2) -> typeEq t1 t2
(TIInpObj t1, TIInpObj t2) -> typeEq t1 t2
_ -> False

hQRName = G.NamedType "query_root"
hMRName = G.NamedType "mutation_root"
tyMsg ty = "types: [" <> namesToTxt ty <>
Expand Down Expand Up @@ -1745,14 +1761,23 @@ mergeMaybeMaps m1 m2 = case (m1, m2) of


-- pretty print GCtx
ppGCtx :: GCtx -> IO ()
ppGCtx gCtx = do
let types = map (G.unName . G.unNamedType) $ Map.keys $ _gTypes gCtx
qRoot = map G.unName $ Map.keys $ _otiFields $ _gQueryRoot gCtx
mRoot = maybe [] (map G.unName . Map.keys . _otiFields) $ _gMutRoot gCtx

print ("GCtx [" :: Text)
print $ " types = " <> show types
print $ " query root = " <> show qRoot
print $ " mutation root = " <> show mRoot
print ("]" :: Text)
ppGCtx :: GCtx -> String
ppGCtx gCtx =
"GCtx ["
<> "\n types = " <> show types
<> "\n query root = " <> show qRoot
<> "\n mutation root = " <> show mRoot
<> "\n subscription root = " <> show sRoot
<> "\n]"

where
types = map (G.unName . G.unNamedType) $ Map.keys $ _gTypes gCtx
qRoot = (,) (_otiName qRootO) $
map G.unName $ Map.keys $ _otiFields qRootO
mRoot = (,) (_otiName <$> mRootO) $
maybe [] (map G.unName . Map.keys . _otiFields) mRootO
sRoot = (,) (_otiName <$> sRootO) $
maybe [] (map G.unName . Map.keys . _otiFields) sRootO
qRootO = _gQueryRoot gCtx
mRootO = _gMutRoot gCtx
sRootO = _gSubRoot gCtx
2 changes: 2 additions & 0 deletions server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ onStart serverEnv wsConn (StartMsg opId q) msgRaw = catchAndIgnore $ do
VT.HasuraType ->
runHasuraQ userInfo gCtx queryParts
VT.RemoteType _ rsi -> do
when (G._todType opDef == G.OperationTypeSubscription) $
withComplete $ sendConnErr "subscription to remote server is not supported"
resp <- runExceptT $ TH.runRemoteGQ httpMgr userInfo reqHdrs
msgRaw rsi opDef
either postExecErr sendSuccResp resp
Expand Down
39 changes: 38 additions & 1 deletion server/src-lib/Hasura/GraphQL/Validate/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Hasura.GraphQL.Validate.Types
( InpValInfo(..)
Expand Down Expand Up @@ -38,6 +39,7 @@ module Hasura.GraphQL.Validate.Types
, fromSchemaDocQ
, TypeMap
, TypeLoc (..)
, typeEq
, AnnGValue(..)
, AnnGObject
, hasNullVal
Expand All @@ -63,6 +65,16 @@ 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
type EqProps a
getEqProps :: a -> EqProps a

typeEq :: (EquatableGType a, Eq (EqProps a)) => a -> a -> Bool
typeEq a b = getEqProps a == getEqProps b

data EnumValInfo
= EnumValInfo
{ _eviDesc :: !(Maybe G.Description)
Expand All @@ -82,11 +94,15 @@ data EnumTyInfo
, _etiLoc :: !TypeLoc
} deriving (Show, Eq, TH.Lift)

instance EquatableGType EnumTyInfo where
type EqProps EnumTyInfo = (G.NamedType, Map.HashMap G.EnumValue EnumValInfo)
getEqProps ety = (,) (_etiName ety) (_etiValues ety)

fromEnumTyDef :: G.EnumTypeDefinition -> TypeLoc -> EnumTyInfo
fromEnumTyDef (G.EnumTypeDefinition descM n _ valDefs) loc =
EnumTyInfo descM (G.NamedType n) enumVals loc
where
enumVals = Map.fromList $
enumVals = Map.fromList
[(G._evdName valDef, fromEnumValDef valDef) | valDef <- valDefs]

data InpValInfo
Expand All @@ -97,6 +113,10 @@ data InpValInfo
-- TODO, handle default values
} deriving (Show, Eq, TH.Lift)

instance EquatableGType InpValInfo where
type EqProps InpValInfo = (G.Name, G.GType)
getEqProps ity = (,) (_iviName ity) (_iviType ity)

fromInpValDef :: G.InputValueDefinition -> InpValInfo
fromInpValDef (G.InputValueDefinition descM n ty _) =
InpValInfo descM n ty
Expand All @@ -120,6 +140,10 @@ data ObjFldInfo
, _fiLoc :: !TypeLoc
} deriving (Show, Eq, TH.Lift)

instance EquatableGType ObjFldInfo where
type EqProps ObjFldInfo = (G.Name, G.GType, ParamMap)
getEqProps o = (,,) (_fiName o) (_fiTy o) (_fiParams o)

fromFldDef :: G.FieldDefinition -> TypeLoc -> ObjFldInfo
fromFldDef (G.FieldDefinition descM n args ty _) loc =
ObjFldInfo descM n params ty loc
Expand All @@ -135,6 +159,11 @@ data ObjTyInfo
, _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))

instance Monoid ObjTyInfo where
mempty = ObjTyInfo Nothing (G.NamedType "") Map.empty

Expand Down Expand Up @@ -172,6 +201,10 @@ data InpObjTyInfo
, _iotiLoc :: !TypeLoc
} deriving (Show, Eq, TH.Lift)

instance EquatableGType InpObjTyInfo where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are comparing the descriptions of the fields in InpObjTyInfo?

type EqProps InpObjTyInfo = (G.NamedType, Map.HashMap G.Name (G.Name, G.GType))
getEqProps a = (,) (_iotiName a) (Map.map getEqProps $ _iotiFields a)

fromInpObjTyDef :: G.InputObjectTypeDefinition -> TypeLoc -> InpObjTyInfo
fromInpObjTyDef (G.InputObjectTypeDefinition descM n _ inpFlds) loc =
InpObjTyInfo descM (G.NamedType n) fldMap loc
Expand All @@ -186,6 +219,10 @@ data ScalarTyInfo
, _stiLoc :: !TypeLoc
} deriving (Show, Eq, TH.Lift)

instance EquatableGType ScalarTyInfo where
type EqProps ScalarTyInfo = PGColType
getEqProps = _stiType

fromScalarTyDef
:: G.ScalarTypeDefinition
-> TypeLoc
Expand Down
31 changes: 29 additions & 2 deletions server/tests-py/graphql_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Hello(graphene.ObjectType):
def resolve_hello(self, info, arg):
return "Hello " + arg

hello_schema = graphene.Schema(query=Hello)
hello_schema = graphene.Schema(query=Hello, subscription=Hello)

class HelloGraphQL(RequestHandler):
def get(self, request):
Expand Down Expand Up @@ -87,7 +87,6 @@ def resolve_allUsers(self, info):

class UserMutation(graphene.ObjectType):
createUser = CreateUser.Field()

user_schema = graphene.Schema(query=UserQuery, mutation=UserMutation)

class UserGraphQL(RequestHandler):
Expand Down Expand Up @@ -125,11 +124,39 @@ def post(self, req):
res = country_schema.execute(req.json['query'])
return mkJSONResp(res)


class person(graphene.ObjectType):
id = graphene.Int(required=True)
name = graphene.String()

def resolve_id(self, info):
return 42
def resolve_name(self, info):
return 'Arthur Dent'

class PersonQuery(graphene.ObjectType):
person_ = graphene.Field(person)

def resolve_person_(self, info):
return person()

person_schema = graphene.Schema(query=PersonQuery)

class PersonGraphQL(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 = person_schema.execute(req.json['query'])
return mkJSONResp(res)

handlers = MkHandlers({
'/hello': HelloWorldHandler,
'/hello-graphql': HelloGraphQL,
'/user-graphql': UserGraphQL,
'/country-graphql': CountryGraphQL,
'/person-graphql': PersonGraphQL
})


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: bulk
args:
- type: run_sql
args:
sql: "drop table person cascade"
Loading