Graphene Bundle¶
Integrates Flask GraphQL and Graphene-SQLAlchemy with Flask Unchained.
Installation¶
Install dependencies:
pip install "flask-unchained[graphene]"
And enable the graphene bundle in your unchained_config.py
:
# project-root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.graphene',
'app',
]
Usage¶
Create a graphene
module in your bundle:
cd your_bundle
mkdir graphene && touch graphene/__init__.py
touch graphene/mutations.py graphene/queries.py graphene/types.py
Let’s say you have some models you want to create a GraphQL schema for:
# your_bundle/models.py
from flask_unchained.bundles.sqlalchemy import db
class Parent(db.Model):
name = db.Column(db.String)
children = db.relationship('Child', back_populates='parent',
cascade='all,delete,delete-orphan')
class Child(db.Model):
name = db.Column(db.String)
parent_id = db.foreign_key('Parent')
parent = db.relationship('Parent', back_populates='children')
Object Types¶
The first step is to define your object types for your models:
# your_bundle/graphene/types.py
import graphene
from flask_unchained.bundles.graphene import SQLAlchemyObjectType
from .. import models
class Parent(SQLAlchemyObjectType):
class Meta:
model = models.Parent
only_fields = ('id', 'name', 'created_at', 'updated_at')
children = graphene.List(lambda: Child)
class Child(SQLAlchemyObjectType):
class Meta:
model = models.Child
only_fields = ('id', 'name', 'created_at', 'updated_at')
parent = graphene.Field(Parent)
Queries¶
Next define the queries for your types:
# your_bundle/graphene/queries.py
import graphene
from flask_unchained.bundles.graphene import QueriesObjectType
from . import types
class Queries(QueriesObjectType):
parent = graphene.Field(types.Parent, id=graphene.ID(required=True))
parents = graphene.List(types.Parent)
child = graphene.Field(types.Child, id=graphene.ID(required=True))
children = graphene.List(types.Child)
When subclassing QueriesObjectType
, it automatically adds default resolvers for you. But these can be overridden if you want, eg:
# your_bundle/graphene/queries.py
import graphene
from flask_unchained import unchained
from flask_unchained.bundles.graphene import QueriesObjectType
from .. import services
from . import types
child_manager: services.ChildManager = unchained.get_local_proxy('child_manager')
class Queries(QueriesObjectType):
# ...
children = graphene.List(types.Child, parent_id=graphene.ID())
def resolve_children(self, info, parent_id=None, **kwargs):
if not parent_id:
return child_manager.all()
return child_manager.filter_by_parent_id(parent_id).all()
Note
Unfortunately, dependency injection does not work with graphene classes. (That said, you can still decorate individual methods with @unchained.inject()
.)
Mutations¶
Graphene mutations, per the RegisterGrapheneMutationsHook
, by default live in the graphene.mutations
module of bundles. This can be customized by setting the graphene_mutations_module_names
attribute on your bundle class.
import graphene
from flask_unchained import unchained, injectable, lazy_gettext as _
from flask_unchained.bundles.graphene import MutationsObjectType, MutationValidationError
from flask_unchained.bundles.security.exceptions import AuthenticationError
from flask_unchained.bundles.security.services import SecurityService, SecurityUtilsService
from flask_unchained.bundles.security.graphene.types import UserInterface
class LoginUser(graphene.Mutation):
class Arguments:
email = graphene.String(required=True)
password = graphene.String(required=True)
success = graphene.Boolean(required=True)
message = graphene.String()
user = graphene.Field(UserInterface)
@unchained.inject('security_service')
def mutate(
self,
info,
email: str,
password: str,
security_service: SecurityService = injectable,
**kwargs,
):
try:
user = LoginUser.validate(email, password)
except MutationValidationError as e:
return LoginUser(success=False, message=e.args[0])
try:
security_service.login_user(user)
except AuthenticationError as e:
return LoginUser(success=False, message=e.args[0])
return LoginUser(success=True, user=user)
@staticmethod
@unchained.inject('security_utils_service')
def validate(
email: str,
password: str,
security_utils_service: SecurityUtilsService = injectable,
):
user = security_utils_service.user_loader(email)
if user is None:
raise MutationValidationError(
_('flask_unchained.bundles.security:error.user_does_not_exist'))
if not security_utils_service.verify_password(user, password):
raise MutationValidationError(
_('flask_unchained.bundles.security:error.invalid_password'))
return user
class LogoutUser(graphene.Mutation):
success = graphene.Boolean(required=True)
@unchained.inject('security_service')
def mutate(self, info, security_service: SecurityService = injectable, **kwargs):
security_service.logout_user()
return LogoutUser(success=True)
class SecurityMutations(MutationsObjectType):
login_user = mutations.LoginUser.Field(description="Login with email and password.")
logout_user = mutations.LogoutUser.Field(description="Logout the current user.")