Hello!
I have created a working example with graphene-mongo and graphene-federation (https://github.com/erebus1/graphene-federation).
We are currently working with GraphQL + Mongoengine. We split up into Microservices, so Federation was the thing to do. After fiddling around for a day, I was able to create a working example with Mongoengine. I want to share it, just in case somebody needs it too.
1. Overview
The example is made up of two Services. Each Service has its own DB. We want to be able to connect these to DBs, but have access via 1 GraphQL Endpoint to be able to connect multiple Frontend Applications. I choose the following file-structure:
gateway
|-gateway.js
graphene_federation
services
|-models
|-ReviewModel.py
|-UserModel.py
|-service1
|-app.py
|-config.py
|-schema.py
|-server.py
|-service2
|-app.py
|-config.py
|-schema.py
|-server.py
2. Models
We have two DB-Models, a User
and a Review
. Each of the models is sitting in its own Database. They are only connected via Mongoengine.
models/ReviewModel.py
from mongoengine import Document
from mongoengine.fields import StringField, ReferenceField
from .UserModel import UserModel
class ReviewModel(Document):
name = StringField()
user = ReferenceField(UserModel)
meta = {'db_alias': 'review-db'}
models/UserModel.py
from mongoengine import Document
from mongoengine.fields import StringField, IntField
class UserModel(Document):
username = StringField()
age = IntField()
meta = {'db_alias': 'user-db'}
3. Services
I wasn't very creative on the service. I just called them service1
and service2
. Service1 is handling the users and Service2 the reviews.
Both have the same structure:
serviceX
|
|-app.py
|-config.py
|-schema.py
|-server.py
server.py
The server.py file is the same in both services, except for the port to avoid conflicts. I am using a UploadView. We are using File uploads, so this a custom option.
from app import app
from graphene_file_upload.flask import FileUploadGraphQLView
from mongoengine import connect
from schema import schema
# we need to connect to both databases
connect('service1',
host=app.config['MONGO_HOST'],
alias='user-db',
username=app.config['MONGO_USER'],
password=app.config['MONGO_PWD'],
authentication_source="admin")
connect('service2',
host=app.config['MONGO_HOST'],
alias='review-db',
username=app.config['MONGO_USER'],
password=app.config['MONGO_PWD'],
authentication_source="admin")
app.add_url_rule('/graphql', view_func=FileUploadGraphQLView.as_view('graphql', schema=schema, graphiql=app.debug))
if __name__ == '__main__':
app.run(port=5000)
app.py
The app.py is even simpler.
from flask import Flask
app = Flask(__name__)
app.debug = True
app.config.from_object('config.DevConfig')
config.py
Nothing special here. Fill in your own details.
class BaseConfig:
TESTING = False
DEBUG = False
MONGO_USER = '******'
MONGO_PWD = '*****'
MONGO_HOST = '*******'
class DevConfig(BaseConfig):
DEBUG = True
schema.py
The schema file is the big difference in the two services. Let's start by the User.
service1
import graphene
from graphene_mongo import MongoengineObjectType
from graphene_federation import build_schema, key
from models.UserModel import UserModel
# The primary key
@key('id')
class User(MongoengineObjectType):
# we resolve a db reference by the id
def __resolve_reference(self, info, **kwargs):
# common mongoengine query
return UserModel.objects.get(id=self.id)
# Use Model as Type (common graphene_mongoengine)
class Meta:
model = UserModel
# define a query
class Query(graphene.ObjectType):
users = graphene.Field(User)
def resolve_users(self, info, **kwargs):
return UserModel.objects.all()
# define a mutation
class CreateUser(graphene.Mutation):
user = graphene.Field(User)
class Arguments:
username = graphene.String()
age = graphene.Int()
def mutate(self, info, username, age):
user = UserModel(username=username, age=age)
user.save()
return CreateUser(user)
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
# build schema USE THE FEDERATION PACKAGE
schema = build_schema(Query, types=[User], mutation=Mutation)
As you can see, nothing special happens here. We just need to set the id
as our key and add a resolver for the key.
Service2 will now be able to resolve a type from another schema by using the external
function.
service2
import graphene
from graphene_mongo import MongoengineObjectType
from graphene_federation import build_schema, key, external, extend
from models.ReviewModel import ReviewModel
# use extend and the key to tell service2 that this a type from another service
@extend('id')
class User(graphene.ObjectType):
# define key, use graphene.ID because this is the type used by graphene_mongo
# set it to external
id = external(graphene.ID())
# set id as key
@key('id')
class Review(MongoengineObjectType):
# Add user as type
user = graphene.Field(User)
# optional: we dont need to resolve the reference
def __resolve_reference(self, info, **kwargs):
return ReviewModel.objects.get(id=self.id)
# Use Model as Type (common graphene_mongoengine)
class Meta:
model = ReviewModel
# define a query
class Query(graphene.ObjectType):
reviews = graphene.Field(Review)
def resolve_reviews(self, info, **kwargs):
return ReviewModel.objects.all().first()
# define a mutation
class CreateReview(graphene.Mutation):
review = graphene.Field(Review)
class Arguments:
name = graphene.String()
user_id = graphene.String()
def mutate(self, info, name, user_id):
review = ReviewModel(name=name, user=user_id)
review.save()
return CreateReview(review)
class Mutation(graphene.ObjectType):
create_review = CreateReview.Field()
# build schema USE THE FEDERATION PACKAGE
schema = build_schema(Query, mutation=Mutation)
4. Gateway
To combine our 2 Services into one gateway, we use the Apollo Gateway.
We just need to create the following file:
gateway.js
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require("@apollo/gateway");
const gateway = new ApolloGateway({
serviceList: [
{ name: 'service1', url: 'http://127.0.0.1:5000/graphql' },
{ name: 'service2', url: 'http://127.0.0.1:5001/graphql' },
// more services
],
});
const server = new ApolloServer({
gateway,
// Currently, subscriptions are enabled by default with Apollo Server, however,
// subscriptions are not compatible with the gateway. We hope to resolve this
// limitation in future versions of Apollo Server. Please reach out to us on
// https://spectrum.chat/apollo/apollo-server if this is critical to your adoption!
subscriptions: false,
});
server.listen().then(({ url }) => {
console.log(`๐ Server ready at ${url}`);
});
Next we need to install the depencies:
npm install @apollo/gateway apollo-server graphql
5. Start it up
The last thing is to just run the two flask applications and start the gateway via
node gateway.js
I hope all the instructions are clear enough. Have a great day!!!