.. DjraphQL documentation master file, created by sphinx-quickstart on Tue Jan 12 21:31:26 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. API documentation ================= .. toctree:: :maxdepth: 4 :caption: Contents: In this section, we'll discuss how to set permissions, customize fields, and keep our GraphQL APIs secure and performant. ``SchemaBuilder`` ***************** The ``SchemaBuilder`` class is the entry-point into DjraphQL. The constructor takes the following arguments: *Keyword args* - ``schema``: Optional. Default value is ``DefaultSchema()``. Must be an instance of a class that implements ``djraphql.schemas.abstract_type_builder.AbstractSchema``. Provide this argument only if the :ref:`type mapping ` used by ``DefaultSchema`` should be :ref:`overridden `, or if another schema should be used altogether (rare). .. _register-entity-classes: ``register_entity_classes()`` -------------------------------------- *Args* - ``*entities``: 1 or more ``Entity`` classes *Returns* - ``None`` Method takes as input the entity classes DjraphQL should expose in the generated GraphQL schema. Can be called multiple times. .. code-block:: python :linenos: :emphasize-lines: 4 from djraphql import SchemaBuilder sb = SchemaBuilder() sb.register_entity_classes(LabelEntity, ArtistEntity) Entity classes passed to ``register_entity_classes`` will affect the result of ``QueryRoot`` and ``MutationRoot``. ``QueryRoot`` ------------- *Returns* - ``graphene.ObjectType`` representing the query root of generated GraphQL schema. Property that returns a Graphene ``ObjectType`` containing operations for reading data. .. code-block:: python :linenos: :emphasize-lines: 6 import graphene from djraphql import SchemaBuilder sb = SchemaBuilder() sb.register_entity_classes(LabelEntity, ArtistEntity) schema = graphene.Schema(query=sb.QueryRoot) ``MutationRoot`` ---------------- *Returns* - ``graphene.ObjectType`` representing the mutation root of generated GraphQL schema. Property that returns a Graphene ``ObjectType`` containing operations for creating, updating or deleting data. .. code-block:: python :linenos: :emphasize-lines: 7 import graphene from djraphql import SchemaBuilder sb = SchemaBuilder() sb.register_entity_classes(LabelEntity, ArtistEntity) schema = graphene.Schema(query=sb.QueryRoot, mutation=sb.MutationRoot) ``registry`` ---------------- *Returns* - ``djraphql.Registry``: object containing generated Graphene types for Django models. Useful when we want to build a custom type that uses a generated type. Both ``QueryRoot`` and ``MutationRoot`` can serve as base classes in situations where we want to build additional, custom types on top of -- or using -- the types generated by DjraphQL. .. code-block:: python :linenos: :emphasize-lines: 6-7,9-10,15,21-22 import graphene from djraphql import SchemaBuilder sb = SchemaBuilder() sb.register_entity_classes(LabelEntity, ArtistEntity) LabelObjectType = sb.registry.get_or_create_type( 'basic_type', model_class=Label) class CustomQueryRoot(sb.QueryRoot): first_label = graphene.Field(LabelObjectType) def resolve_first_label(parent, info): return Label.objects.first() class CustomMutationRoot(sb.MutationRoot): a_custom_field = graphene.String() def resolve_a_custom_field(parent, info): return "A custom string field" schema = graphene.Schema(query=CustomQueryRoot, mutation=CustomMutationRoot) ``Entity`` ****************** Creating and :ref:`registering ` classes that inherit from ``Entity`` is how you build your GraphQL schema. ``Meta`` class -------------- The nested ``Meta`` class contains items that affect the shape of the GraphQL types being created. These items are accessed only at schema build time (vs. query resolution time). Each entity must define a ``Meta`` class. If it is not present, an error will be thrown at schema-build time. ``model`` ######### Required. Set its value to the Django ``Model`` class for which we're building GraphQL types. .. code-block:: python :linenos: :emphasize-lines: 6 from djraphql import Entity from songs.models import Album class AlbumEntity(Entity): class Meta: model = Album After registering ``AlbumEntity``, our ``SchemaBuilder`` instance's ``QueryRoot`` property will contain operations for reading ``Albums``. ``fields`` ########## Required. Set its value to a tuple containing instances of ``ModelField``, ``ComputedField``, or ``AllModelFields``. Defines what fields are defined on the generated GraphQL type. ``ModelField`` ~~~~~~~~~~~~~~ Pass an instance of this class to the ``fields`` property of an entity's ``Meta`` nested class. This will result in exposing the specified model field. Primary key fields are read-only. *Args* - Name of the field we want to expose *Keyword args* - ``read_only``: Optional, default ``False`` . Pass ``True`` if attribute should be treated as read-only. - Additionally useful for fields whose value is populated automatically, such as ``created_date``. - ``graphene_type``: Optional, default ``None`` . Pass the Graphene type of this field if the default type is not correct. As an example, we may pass ``ModelField('id', graphene_type=graphene.Int)`` if we want our primary-key value to be exposed as an ``Int`` and not the default type of ``ID``. *Keyword args* - ``read_only``: ``True`` if the field should be read-only. ``False`` by default. .. code-block:: python :linenos: :emphasize-lines: 8-10 from djraphql import Entity, ModelField from songs.models import Album class AlbumEntity(Entity): class Meta: model = Album fields = ( ModelField('id'), ModelField('title'), ModelField('released_date', read_only=True), ) In this case, the only ``Album`` model fields exposed in our API will be ``id``, ``title``, and ``released_date``, which will be read-only. This allow-list behavior makes for a sane default: we must explicitly declare what fields we want to expose via ``ModelField``\s. This avoids surprises by ensuring we don't accidently expose a column we shouldn't have. ``AllModelFields`` ~~~~~~~~~~~~~~~~~~ *Keyword args* - ``excluding``: Set to a tuple containing the names of the fields we want **do not want to expose**. There are cases where we just want to expose everything on a model without the tedium of maintaining an explicit list. Or perhaps instead, we want to expose everything *except* a few fields (a deny-list). The ``AllModelFields`` class can be used to accomplish these use cases. Placing an ``AllModelFields`` instance in our ``fields`` tuple is equivalent to defining a ``ModelField`` for every field on our model. .. code-block:: python :linenos: :emphasize-lines: 8 from djraphql import Entity, ModelField, AllModelFields from songs.models import Album class AlbumEntity(Entity): class Meta: model = Album fields = ( AllModelFields(excluding=('artists',)), ModelField('released_date', read_only=True), ) This code is equivalent to the example in the ``ModelField`` section above. We're passing an ``AllModelFields`` instance, which saves us some key-strokes, but more importantly, allows us to opt-in to the deny-list behavior. By passing the ``excluding`` keyword argument, we can ensure we do not expost the ``artists`` field. We still declare ``released_date`` as read-only via the same mechanism as above. The priority of a field defined explicitly (via ``ModelField``) is higher than that of a field defined implicitly (via ``AllModelFields``). If a field exists in the ``excluding`` keyword argument *and* has a ``ModelField`` defined, an error will be thrown during schema generation. ``ComputedField`` ~~~~~~~~~~~~~~~~~~ *Args* - A string specifying the name of our property. E.g. ``my_property`` - The field's Graphene type. E.g. ``graphene.Int``, ``graphene.String``. *Keyword args* - ``depends_on``: An enumerable of field names that the ``ComputedField`` depends on. Useful when we define a ``ComputedField`` whose handler depends on a field that was not specified in our GraphQL query. In such a scenario, Django's ORM will lazily fetch that field's value from the database upon accessing it within the handler, which can result in an N+1 scenario. By passing ``depends_on=["some_dependency"]``, DjraphQL will ensure the field will be populated before passing the instance to your handler. A common requirement for an API author is the ability to expose a read-only field which does not map directly to a backing column. This allows for derived or calculated data to be part of the API response in a way that is ergonomic to the API consumer. We can achieve this by passing a ``ComputedField`` instance in our ``fields`` tuple. For each ``ComputedField``, we must define a method on our entity that DjraphQL can call to obtain the value. .. code-block:: python :linenos: :emphasize-lines: 13,16-18 import graphene from djraphql import Entity, from djraphql.fields import ( ModelField, AllModelFields, ComputedField) from songs.models import Album class AlbumEntity(Entity): class Meta: model = Album fields = ( AllModelFields(excluding=('artists',)), ModelField('released_date', read_only=True) ComputedField('star_count', graphene.Int), ) @staticmethod def get_star_count(context, album): return album.compute_the_star_count() Here we expose a ``starCount`` field on our ``Album`` GraphQL type, even though the ``Album`` Django model has no such field. ``access_permissions`` ---------------------- Optional. Set its value to a tuple of :ref:`PermissionFlag ` instances. Can be used to fulfill schema-level and request-level permissioning requirements. ``filter_backends`` ------------------- Optional. Set its value to a tuple of :ref:`FilterBackends ` instances. Can be used to ensure the correct conditions are always added to the where-clause for any executed SQL relating to the model. ``custom_node_name`` -------------------- Optional. A string to specify a custom name for the node in the schema. This is specially helpful to prevent Model name collisions. > Take in mind that this custom name will be used for all the types generated by DjraphQL. ``get_for_insert()`` -------------------- *Args* - ``context``: the Django request object *Returns* - An instance of the entity's model Optional. Override this static method to control how this model is set for mutations that insert new data. As an example, say we issue a GraphQL mutation for inserting a ``Playlist``. We don't want the client to determine what value to set for ``Playlist.team_id``, because then they could create ``Playlist``\s associated with teams outside of their own! To prevent this, we can use ``get_for_insert`` to ensure that any time we are creating an object that references a ``Team``, that the object's ``team_id`` is set from the authenticated request. .. code-block:: python :linenos: :emphasize-lines: 8-10 from djraphql import Entity from auth.models import Team class TeamEntity(Entity): class Meta: model = Team @staticmethod def get_for_insert(context): return context.user.team Our ``get_for_insert`` method will be called before inserting an object that has a ``team_id`` field. The value returned by our method will always "win" no matter what's passed by the API consumer. .. note:: Defining ``get_for_insert`` on an ``Entity`` will have the effect that its referencing fields will be read-only. For example, since our ``TeamEntity`` defines ``get_for_insert``, API consumers will be unable to set or change the value of any other model's ``team_id``. ``before_insert()`` ------------------- *Args* - ``data``: dictionary of values representing the object to be inserted *Returns* - ``None`` Optional. Override this static method to alter the object being inserted or e.g. for logging or metrics purposes. The return value is unused. .. code-block:: python :linenos: :emphasize-lines: 9-15 import logger from djraphql import Entity from auth.models import Team class Team(Entity): class Meta: model = Team @staticmethod def before_insert(data, context): user_email = context.user.email logger.info( f'{user_email} creating Team {data["name"]}.' ) # Or, we could mutate data before it is inserted! This callback is executed before the data's relations have been inserted as well. ``after_insert()`` ------------------- *Args* - ``instance``: object that has been inserted *Returns* - ``None`` Optional. Override this static method to perform operations that must happen after insertion of an object. For example, logging or emitting metrics. The return value is unused. .. code-block:: python :linenos: :emphasize-lines: 9-14 import logger from djraphql import Entity from auth.models import Team class Team(Entity): class Meta: model = Team @staticmethod def after_insert(instance, context): user_email = context.user.email logger.info( f'{user_email} created Team {instance.name}.' ) This callback is executed after the instance's relations have been inserted as well. .. _filterbackend: ``FilterBackend`` ***************** By subclassing ``FilterBackend`` and overriding its ``filter_backend`` method, we can consistently and automatically apply the correct ``WHERE`` clause to each query we execute in the process of resolving a GraphQL request. As an example, let's say we have a business rule that states that ``Playlist``\s created by a user on one ``Team`` cannot be accessed by users on any other ``Team``\s. We can fulfill this requirement via a ``FilterBackend``. .. code-block:: python :linenos: :emphasize-lines: 2,4-11,17 from djraphql import Entity, FilterBackend from auth.models import Playlist class IsSameTeam(FilterBackend): def __init__(self, django_path_to_team_id): self.django_path_to_team_id = django_path_to_team_id def filter_backend(self, request, queryset): return queryset.filter(**{ self.django_path_to_team_id: request.user.team_id }) class PlaylistEntity(Entity): class Meta: model = Playlist filter_backends = (IsSameTeam('user__team_id'),) Any time a request contains a query that accesses a ``Playlist`` (directly or indirectly via a relationship from another model), the executed SQL will contain an inner-join of ``Playlist`` to ``User`` and the where-clause will contain ``AND user.team_id = {team_id from request}``, thus ensuring we cannot leak ``Playlist`` data across ``Team``\s! Additionally, mutations will check that any specified relational (e.g. via ``team_id``) model is returned by its entity's ``FilterBackend``\s, keeping our data secure. ``validator`` ************** .. note:: Validation is heavily dependent on individual use-case, API requirements, and schema. For this reason, DjraphQL's philosophy is to be as unopinionated as possible. Thus, the constructs below are simply generic hooks that allow you to validate input and -- if necessary -- halt the mutation and return errors. How those errors get returned to the API consumer is largly a *schema-level* concern: DjraphQL's Entity API makes no assumptions about the response shape, leaving that to the schema. Head over to the :ref:`validation section ` to see how the default schema handles validation errors. ``Entity.validator`` can be set to any object that contains a ``validate`` method, which must be implemented by the library user. ``validate()`` -------------- *Args* - A ``ValidatableInput`` object that can be used to obtain the data being validated. *Returns* - A tuple of ``(boolean, dict|list)``. The first item needs to be ``True`` if the mutation should be allowed, otherwise ``False``. The second item is the value that will be returne to the API consumer. ``ValidatableInput`` -------------------- An object provided by the *schema* that provides a view of the input data. It exposes a single method, ``to_value``, which is implemented by the schema. See :ref:`validatable input ` to learn more. Validation example ------------------ Let's illustrate how validation works with an example. .. code-block:: python :linenos: :emphasize-lines: 4-15,21 from djraphql import Entity from auth.models import Album class AlbumValidator: def validate(self, validatable_input): input_value = validatable_input.to_value() errors = {} if not input_value['released_date']: errors['released_date'] = 'Enter a valid date.' if not input_value['title']: errors['title'] = 'Enter a valid title.' is_valid = bool(errors) return (is_valid, errors) class AlbumEntity(Entity): class Meta: model = Album validator = AlbumValidator() To see how errors are returned to the API user, check out the :ref:`validation errors ` section of the *Default schema* page. .. _permission-flag: ``PermissionFlag`` ****************** ``Entity.access_permissions`` is defined as a tuple of ``PermissionFlag`` instances, which provides control over what operations are included in the schema, and whether or not a request can access those operations. The ``djraphql.access_permissions`` module exports four classes that inherit ``PermissionFlag``: - ``Create`` - ``Read`` - ``Update`` - ``Delete`` Schema-level control -------------------- The default value for ``access_permissions`` is ``(Read(),)``, which means the model will be read-only: no create, update, or delete operations will exist in the GraphQL schema generated by DjraphQL. We can provide an ``access_permissions`` field on our entity to change that. By providing a tuple of the ``PermissionFlag`` instances representing the operations we want on our model, we can control whether or not certain operations are defined. .. code-block:: python :linenos: :emphasize-lines: 2-3,10 from djraphql import Entity from djraphql.access_permissions import ( Create, Read, Update, Delete) from auth.models import Album class AlbumEntity(Entity): class Meta: model = Album access_permissions = (Create(), Read(), Update(), Delete()) The above code will fully expose ``Album`` for create-, read-, update- and delete-access via our GraphQL schema. Short-hand flags ################ If you prefer terseness, the ``djraphql.access_permissions`` defines a single-character field for each operation. - ``C = Create()`` - ``R = Read()`` - ``U = Update()`` - ``D = Delete()`` The above code snippet could have defined ``access_permissions = (C, R, U, D)``, which some developers find a bit more readable. Request-level control --------------------- The ``PermissionFlag`` constructor accepts zero or more instances of ``RequestAuthorizationCheck``. For more granular access control, you can define your own classes that inherit ``RequestAuthorizationCheck`` and pass instances to the ``PermissionFlags`` in your ``access_permissions`` field. .. code-block:: python :linenos: :emphasize-lines: 4,8-10,17-19 from songs.models import Album from djraphql import Entity from djraphql.access_permissions import ( Create, R, Update, RequestAuthorizationCheck ) from djraphql import SchemaBuilder class IsAdmin(RequestAuthorizationCheck): def is_authorized(self, context): return context.user.is_admin class AlbumEntity(Entity): class Meta: model = Album access_permissions = ( Create(IsAdmin()), R, Update(IsAdmin()) ) The ``IsAdmin`` above class verifies that a user is an adminstrator by overriding the ``is_authorized`` method. By passing an instance of ``IsAdmin`` to the ``Create`` and ``Update`` flags, we're ensuring that everyone can *read* ``Album``\s, but only administrators can create or update them. ``RequestAuthorizationCheck`` ################################## Create classes that inherit from ``RequestAuthorizationCheck`` to perform request-level verification that a user can access a create, read, update, or delete operation. The only requirement is that the class must provide a ``is_authorized(self, context)`` method. .. code-block:: python :linenos: :emphasize-lines: 4-9,15 from djraphql.access_permissions import ( RequestAuthorizationCheck) class UserHasRole(RequestAuthorizationCheck): def __init__(self, *roles): self.roles = roles def is_authorized(self, context): return context.user.role in self.roles class AlbumEntity(Entity): class Meta: model = Album access_permissions = (Read(UserHasRole('admin', 'staff')),) In this example, ``Album`` can only be read if the user associated with the request has the ``admin`` or ``staff`` role.