Run pip install baseapp-backend[graphql]
And make sure to add the frozen version to your requirements/base.txt file
- Make sure to add
graphene_djangotoINSTALLED_APPS. - Add
GRAPHENEto yoursettings/base.py:
# GraphQL
GRAPHENE = {
"SCHEMA": "apps.graphql.schema",
"MIDDLEWARE": (
"graphene_django.debug.DjangoDebugMiddleware",
"baseapp_core.graphql.LogExceptionMiddleware",
"baseapp_core.graphql.TokenAuthentication",
"baseapp_api_key.graphql.APIKeyAuthentication",
),
"SCHEMA_OUTPUT": "schema.graphql",
}- Create file
apps/graphql.pywith:
import graphene
from baseapp_auth.graphql import UsersQuery
from graphene_django.debug import DjangoDebug
class Query(
graphene.ObjectType,
UsersQuery
):
debug = graphene.Field(DjangoDebug, name="_debug")
schema = graphene.Schema(query=Query)- Add the path in your
urls.py
from baseapp_core.graphql import GraphQLView
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
# ...
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]Our GraphQLView is a subclass of graphene_django.views.GraphQLView with some additional features:
- Sentry integration, it will name the transaction with the query name instead of just
/graphql, making it easy to find queries on Sentry.
To enable websockets you need to make sure you have daphne in your INSTALLED_APPS and ASGI_APPLICATION setup in your settings file.
INSTALLED_APPS = [
"daphne",
# ...
]
ASGI_APPLICATION = "apps.asgi.application"In your asgi.py make sure to have something like:
from django.core.asgi import get_asgi_application
from django.urls import re_path
from channels.routing import ProtocolTypeRouter, URLRouter
django_asgi_app = get_asgi_application()
# we need to load all applications before we can import from the apps
from baseapp_core.graphql.consumers import GraphqlWsJWTAuthenticatedConsumer
# OR if not using JWT:
# from baseapp_core.graphql.consumers import GraphqlWsAuthenticatedConsumer
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": URLRouter([
re_path(r"graphql", GraphqlWsJWTAuthenticatedConsumer.as_asgi())
]),
}
)Make sure to check that when running runserver if you see the following message, this will confirm you are using ASGI:
[daphne.server] [INFO] Listening on TCP address 0.0.0.0:8000
Create your first DjangoObjectType in apps/[app_name]/graphql/object_types.py, like:
from baseapp_core.graphql import DjangoObjectType
class UserNode(DjangoObjectType):
class Meta:
model = User
fields = ("id", "username", "email", "first_name", "last_name")ObjectTypes that inherit from DjangoObjectType will have the following fields:
id- Relay global id, base64 of{ObjectType}:{pk}pk- Same as your model's primary key
All connections with this ObjectType will inherit CountedConnection, which will add the following fields to the connection type:
totalCount- Total number of objects in the database for this queryedgesCount- Number of objects in this page
And will have the same name of the model, doens't better whats the ObjectType's class name, e.g.:
class MyObjectType(DjangoObjectType):
class Meta:
model = MyModelYour GraphQL schema will have a MyModel type. You can still override with a name attribute if necessary:
class MyObjectType(DjangoObjectType):
class Meta:
model = MyModel
name = "MyCustomName"Create your first mutation in apps/[app_name]/graphql/mutations.py, like:
from baseapp_core.graphql import RelayMutation
from .object_types import UserNode
class ChangePassword(RelayMutation):
ok = graphene.Boolean(required=True)
class Input:
old_password = graphene.String(required=True)
new_password = graphene.String(required=True)
class UserMutations:
change_password = ChangePassword.Field()By inherinting RelayMutation your mutation will have the following fields:
clientMutationId- Relay client mutation iderrors- List of errors, if any_debug- Debug information, only available ifDEBUG=True
Add RelayModel to your model's inheritance, like:
from baseapp_core.models import RelayModel
class User(RelayModel):
# ...This will add the following methods and properties to your model:
relay_id- Relay global ID property, base64 of{ObjectType}:{pk}get_graphql_object_type- Class method that, return the model'sDjangoObjectTypeclass
So you can access the relay id of your model like:
user = User.objects.get(pk=1)
user.relay_idAdd baseapp_core.graphql.relay.Node.Field to your Query class, like:
from baseapp_core.graphql import Node
class Query(
graphene.ObjectType,
UsersQuery
):
user = Node.Field(UserObjectType)This will make it possible to retrieve objects by both relay_id and your models's pk, e.g.:
query {
byRelayId: user(id: "VXNlcjox") {
id
pk
username
}
byPk: user(id: "1") {
id
pk
username
}
}Aboth will return the same object.
Add baseapp_core.graphql.mutations.DeleteNode to your Mutation class, like:
from baseapp_core.graphql import DeleteNode
class Mutation(
graphene.ObjectType,
):
delete_node = DeleteNode.Field()This will make it possible to delete any object that the user has permission to delete:
mutation {
deleteNode(id: "VXNlcjox") {
deletedID @deleteRecord
}
}Generate a relay id from a model instance, e.g.:
from baseapp_core.graphql import get_obj_relay_id
user = User.objects.get(pk=1)
get_obj_relay_id(user)Get a model instance from a relay id, e.g.:
from baseapp_core.graphql import get_obj_from_relay_id
user = get_obj_from_relay_id(info, "VXNlcjox")Where info is an instance of graphene.ResolveInfo passed to your resolver.
Get a model's pk from a relay id, e.g.:
from baseapp_core.graphql import get_pk_from_relay_id
pk = get_pk_from_relay_id("VXNlcjox")Add ThumbnailField to your model, like:
from baseapp_core.graphql import ThumbnailField
class User(RelayModel):
selfie = ThumbnailField()Then we can query by specific size, like:
query {
user(id: "1") {
selfie(width: 100, height: 100) {
url
}
}
}Add login_required to your mutation, like:
from baseapp_core.graphql import login_required
class ChangePassword(RelayMutation):
ok = graphene.Boolean(required=True)
class Input:
old_password = graphene.String(required=True)
new_password = graphene.String(required=True)
@classmethod
@login_required
def mutate_and_get_payload(cls, root, info, **input):
# ...Returns a function that will return the DjangoObjectType class for a model, like:
import swapper
from baseapp_core.graphql import get_object_type_for_model
Profile = swapper.load_model("baseapp_profiles", "Profile")
class UserObjectType(DjangoObjectType):
profile = graphene.Field(get_object_type_for_model(Profile))
class Meta:
model = UserMake sure to add to your app's confitest.py:
from baseapp_core.tests.fixtures import * # noqa
from baseapp_core.graphql.testing.fixtures import * # noqaThen you can use the following fixtures:
Args:
query: (string) - GraphQL query to runoperation_name: (string) - If the query is a mutation or named query, you must supply the operation_name. For annon queries ("{ ... }"), should be None (default).input_data: (dict) - If provided, the $input variable in GraphQL will be set to this value. If bothinput_dataandvariables, are provided, theinputfield in thevariablesdict will be overwritten with this value.variables: (dict) - If provided, the "variables" field in GraphQL will be set to this value.headers: (dict) - If provided, the headers in POST request to GRAPHQL_URL will be set to this value. Keys should be prepended with "HTTP_" (e.g. to specify the "Authorization" HTTP header, use "HTTP_AUTHORIZATION" as the key).client: (django.test.Client) - Test client. Defaults to django.test.Client.graphql_url: (string) - URL to graphql endpoint. Defaults to "/graphql".
Returns:
Responseobject from client
To make request as a user.
Args are the same as graphql_client, but will inject django_user_client as the client argument.
For testing websockets its necessary to use the following in the begining of your tests file:
pytestmark = pytest.mark.django_db(transaction=True)Its wise not to mix websocket tests with other tests, since it will make the tests run slower.
For testing websockets we have the following fixtures:
Args:
consumer_attrs:GraphqlWsConsumerattributes dict. Optional.communicator_kwds: Extra keyword arguments for the Channelschannels.testing.WebsocketCommunicator. Optional.
Example:
@pytest.mark.asyncio
async def test_user_recieves_new_notification_subscription_event(
django_user_client, graphql_ws_user_client
):
# Establish & initialize WebSocket GraphQL connection.
client = await graphql_ws_user_client(consumer_attrs={"strict_ordering": True})
# Subscribe to GraphQL subscription.
sub_id = await client.send(
msg_type="subscribe",
payload={
"query": SUBSCRIPTION_QUERY,
"operationName": "op_name",
},
)
await client.assert_no_messages()
notification = await database_sync_to_async(NotificationFactory)(
recipient=django_user_client.user
)
# Check that subscription message were sent.
resp = await client.receive(assert_id=sub_id, assert_type="next")
assert (
resp["data"]["onNotificationChange"]["createdNotification"]["node"]["id"]
== notification.relay_id
)
# Disconnect and wait the application to finish gracefully.
await client.finalize()Same as graphql_ws_user_client, but as an anonymous user.