.. 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. Default schema ============== DjraphQL examines a set of Django models and generates types that map to the types of a GraphQL API. The actual generation of those types is the concern of a *schema*. DjraphQL ships with a single schema, which we'll refer to as the *default schema*. In this section, we'll enumerate what types are generated for an entity class. .. note:: We'll use an asterisk (``*``) to represent the name of the model that the operation interacts with. For example, if we have an ``Entity`` whose nested meta class has ``model = Foo`` defined, a root query operation called ``FooByPk`` will be generated. Root queries ************ Two root query objects can be generated for each entity. For instance, if we've defined an ``AlbumEntity`` for our ``Album`` model, our GraphQL schema will have the following root query objects. The ``Entity`` class must be readable, i.e. ``access_permissions`` contains a ``Read()`` instance. ``*ByPk`` --------- *Args* - ``pk`` is the primary key of the ``Album`` being fetched *Returns* - ``Album`` object matching the given primary key ``*sMany`` ---------- *Args* - ``where: WhereAlbum`` can be used to craft a set of conditions in a SQL where-clause - ``orderBy: [OrderByAlbum]`` can define how the list is ordered - ``limit: Int`` can define how many items are returned. The default value is 50. - ``offset: Int`` can define how many items to skip within the resulting SQL query before returning the list of items. Can be used in conjunction with ``limit`` to paginate items. *Returns* - ``[Album!]`` objects matching the provided filtering parameters Root mutations ************** Depending on the values specified in an entity's ``access_permissions`` field, there are three root mutation types for a given model. ``insert*`` ----------- The ``Entity`` class must be creatable, i.e. ``access_permissions`` contains a ``Create`` instance. *Args* - ``data: AlbumInput!`` is the primary key of the ``Album`` being fetched - ``tag: String`` can be used to map inserted objects back to their tags via ``pksToTags`` *Returns* - ``InsertAlbum`` result object of the insert mutation This operation can insert nested objects as well. If we want to insert a ``Label`` and its ``Artist``\s in the same request, we can do so. .. code-block:: graphql mutation { insertLabel( tag: "label-tag" data: { name: "My Label" establishedYear: 2021 artists: [ { tag: "first-artist-tag", data: {name: "First Artist"} }, { tag: "second-artist-tag", data: {name: "Second Artist"} } ] } ) { success pksToTags { pk tag } result { id artists { id } } } } .. note:: DjraphQL's concept of *tags* are meant to answer the following question: - What is the primary-key of an inserted object with client-side identifier X? The ``pksToTags`` field is a map of all tags given in the input to the primary-keys of their inserted objects. So in our example above, the ``pksToTags`` field would look something like: ``[{"tag": "label-tag", "pk": 1}, {"tag": "first-artist-tag", "pk": 1}, {"tag": "second-artist-tag", "pk": 2}]`` In addition to ``tag`` and ``pk``, there's a third property, ``type``, that indicates which model was inserted. If your use case does not involve nested inserts or client-side IDs, you can largely ignore tags. ``update*`` ----------- The ``Entity`` class must be updatable, i.e. ``access_permissions`` contains an ``Update`` instance. *Args* - ``pk`` (``Int``) is the primary key of the item being updated - ``data`` (``AlbumInput!``) is an object specifying the updated fields *Returns* - ``UpdateAlbum`` result object of the update mutation Similar to the insert mutation, this operation can update (*and* insert!) nested objects. If we want to update a ``Label`` and its ``Artist``\s in the same request, we can do so. .. code-block:: graphql mutation { updateLabel( pk: 1 data: { name: "My Cooler Label" establishedYear: 3021 artists: { updateStrategy: "merge" items: [ { pk: 1, data: {name: "Cool Artist"} }, { tag: "second-artist-tag" data: {name: "Cooler Artist"} } ] } } ) { success pksToTags { pk tag } result { id artists { id } } } } In this scenario, there's more going on, so let's walk through it. First, our update mutation requires the ``pk`` of the item being updated. This also implies we don't need to pass a ``tag`` (because we already know the PK!). Then, for our nested update of ``artists`` field, note that we're passing an object instead of a list. This is because we can choose how the update works via our ``updateStrategy``. The choices are: - ``merge`` (default) updates the items with a ``pk`` and inserts items without a ``pk``. - ``replace`` does the same thing as ``merge``, but additionally **deletes** existing items that aren't represented (by their ``pk``) in the ``items`` list. - This option is quite useful in the case where we want our database to match our list exactly, but **be careful because it is destructive**! .. note:: Errors will be thrown if a nested update attempts to perform an insert, update, or delete on a model whose entity class has not speecified the corresponding access permissions (``Create()``, ``Update()``, ``Delete()``). Since we've chosen ``merge``, our first artist item will be updated because we passed its primary-key (``pk``). The second artist will be inserted, and ``pksToTags`` will contain an entry for that insertion. ``delete*`` ----------- The ``Entity`` class must be deletable, i.e. ``access_permissions`` contains an ``Delete`` instance. *Args* - ``pk`` (``Int``) is the primary key of the item being updated *Returns* - ``DeleteAlbum`` result object of the delete mutation .. _type-mapping: Type mappings ************* A schema defines the mapping between Django model field types and the GraphQL types they are exposed as in the generated schema. Below is that mapping: .. code-block:: python { "AutoField": graphene.ID, "BigAutoField": graphene.ID, "IntegerField": graphene.Int, "PositiveIntegerField": graphene.Int, "BigIntegerField": graphene.Int, "ForeignKey": graphene.ID, "OneToOneField": graphene.ID, "ManyToManyField": graphene.ID, "DecimalField": graphene.Float, "FloatField": graphene.Float, "DateField": graphene.Date, "DateTimeField": graphene.DateTime, "CharField": graphene.String, "TextField": graphene.String, "BooleanField": graphene.Boolean, "NullBooleanField": graphene.Boolean, } .. _overriding-the-mapping: Overriding the mapping ---------------------- It is sometimes useful to override the mapping defined above. For example, if we want all primary- or foreign-key (``*id``) fields in our API to be ``Int`` instead of ``ID``. It is possible to accomplish this for an *individual field* by passing ``graphene_type`` to each ``ModelField`` that represents a relational field. But to do this for every field on every model is quite cumbersome. Instead, we can instruct DjraphQL to treat all ``AutoField``, ``OneToOneField``, etc. as ``Int`` types by directly instantiating the schema object and passing it to the SchemaBuilder via the ``field_type_map_overrides`` keyword argument. Please note there are issues with taking this approach specifically for ``BigAutoField``. This is because these fields are 64-bit integers, but Graphene only represents 32-bit integers with its ``Int`` type. Instead, best practice would be to override ``BigAutoField`` to map to ``String`` and accomodate this serialization in your application. .. code-block:: python :linenos: :emphasize-lines: 3,6-9 from graphene import Int from djraphql import SchemaBuilder from djraphql.schemas.default import DefaultSchema my_schema = DefaultSchema( field_type_map_overrides={ 'AutoField': Int, 'BigAutoField': String, 'OneToOneField': Int, } ) schema_generator = SchemaBuilder(schema=my_schema) Filtering via ``where`` *********************** The schema generated by DjraphQL is quite flexible: it can craft arbitrarily complex ``WHERE`` and ``ORDER BY`` clauses as well as define a ``LIMIT`` and ``OFFSET``. Predicates ---------- The ``where`` keyword provides the following constructs that allow us to craft most any filtering logic. Equality (``_eq``) ################## .. code-block:: graphql query { AlbumsMany(where: {title: {_eq: "Cool Album"}}) { id } } Inequality (``_neq``) ##################### .. code-block:: graphql query { AlbumsMany(where: {title: {_neq: "Cool Album"}}) { id } } Greater than (``_gt``) ###################### .. code-block:: graphql query { AlbumsMany(where: {releasedDate: {_gt: "1970-01-01"}}) { id } } At least (``_gte``) ################### A.K.A. *greater than or equal to* .. code-block:: graphql query { AlbumsMany(where: {releasedDate: {_gte: "1970-01-01"}}) { id } } Less than (``_lt``) ################### .. code-block:: graphql query { AlbumsMany(where: {releasedDate: {_lt: "1970-01-01"}}) { id } } At most (``_lte``) ################## A.K.A. *less than or equal to* .. code-block:: graphql query { AlbumsMany(where: {releasedDate: {_lte: "1970-01-01"}}) { id } } List has (``_in``) ####################### .. code-block:: graphql query { AlbumsMany(where: {title: {_in: ["Fun", "Exciting"]}}) { id } } List has not (``_nin``) ####################### .. code-block:: graphql query { AlbumsMany(where: {title: {_nin: ["Sad", "Glum"]}}) { id } } Is empty (``_is_null``) ####################### .. code-block:: graphql query { AlbumsMany( where: { title: { _is_null: true} # Pass False for "is not null" # title: { _is_null: false} } ) { id } } String contains (``_like``) ########################### .. code-block:: graphql query { AlbumsMany(where: {title: {_like: "Love"}}) { id } } ``_like`` is a case-sensitive search. To perform a case-insensitive search, use ``_ilike``. Logical operands ---------------- Combining predicates via ``AND`` and ``OR`` is also possible via ``_and`` & ``_or``, which themselves are predicates, and thus can be nested to produce complex filtering. Additionally, ``_not`` can be used to negate predicates. Conjunction (``_and``) ###################### The ``_and`` keyword can be used to join a list of predicates with a logical ``AND`` operation. .. code-block:: graphql query { AlbumsMany( where: { _and: [ {title: {_like: "Love"}} {releasedDate: {_gte: "1970-01-01"}} ] # Equivalent, less verbose/explicit: # {title: {_like: "Love"}, releasedDate: {_gte: "1970-01-01"}} } ) { id } } Disjunction (``_or``) ###################### The ``_or`` keyword can be used to join a list of predicates with a logical ``OR`` operation. .. code-block:: graphql query { AlbumsMany( where: { _or: [ {title: {_like: "Love"}}, {releasedDate: {_gte: "1970-01-01"}} ] } ) { id } } Negation (``_not``) ################### The ``_not`` keyword can be used to negate the value of a predicate. .. code-block:: graphql query { AlbumsMany( where: { _not: { _or: [ {title: {_like: "Love"}}, {releasedDate: {_gte: "1970-01-01"}} ] } } ) { id } } Here we're geting all Albums that released before 1970 that don't contain *Love* in the title. Nested relations ---------------- Queries can filter on arbitrarily nested relations. For example, we can ask our GraphQL endpoint to give us all ``Album``\s that belong to ``Artist``\s that belong to a set of ``Label``\s: .. code-block:: graphql query AlbumsOnLabels($labelIds: [Int!]) { AlbumsMany( where: { artist: {label: {id: {_in: $labelIds}}} # Also works: # artist: {labelId: {_in: $labelIds}} } ) { id } } Sort order ********** Use the ``orderBy`` parameter to sort results. .. code-block:: graphql query { AlbumsMany( orderBy: [ {title: asc}, {releasedDate: desc} ] ) { id } } Pagination ********** Using ``limit`` and ``offset`` together allows for pagination of results. ``limit`` --------- Use the ``limit`` parameter to specifiy the number of desired results. The default value is 50. .. code-block:: graphql query { AlbumsMany( orderBy: [{title: asc}, {releasedDate: desc}] limit: 100 ) { id } } ``offset`` ---------- Use the ``offset`` parameter to specifiy the number of results to skip. The default value is 0. .. code-block:: graphql query { AlbumsMany( orderBy: [{title: asc}, {releasedDate: desc}] limit: 100 offset: 100 ) { id } } Distinct ******** Use the ``distinct`` parameter to return only distinct rows. This can be used to avoid duplicate objects when we're querying across relations with e.g. ``_is_null``. Let's query for all ``Label`` objects that have signed at least one ``Artist``, and for each label, return the top 3 artists ordered alphabetically by name: .. code-block:: graphql query { LabelsMany( where: {artists: {id: {_is_null: false}}} ) { id artists(limit: 3 orderBy: {name: asc}) { id name } } } If one of our labels has many artists, the resulting ``LabelsMany`` list will unfortunately contain a duplicate label item for each artist. The ``distinct`` keyword can help. .. code-block:: graphql :emphasize-lines: 4 query { LabelsMany( where: {artists: {id: {_is_null: false}}} distinct: true ) { id artists(limit: 3 orderBy: {name: asc}) { id name } } } This will ensure our result contains no duplicate label items, and our result will look like we expect. Filtering on the result-set *************************** It is also possible to filter via ``where`` on *to-many* relations in the resultset. This is useful when we want to e.g. get the 3 most recent albums for each artist signed by the Parlophone label. .. code-block:: graphql query { ArtistMany(where: {label: {name: {_eq: "Parlophone"}}}) { id name albums(orderBy: {releasedDate: desc} limit: 3) { id title } } } .. _schemas-validation: Validation ********** .. note:: Because validation is heavily use case dependent, DjraphQL simply exposes hooks that enable the library user to easily validate input and return errors if necessary. *How* validation is performed is left up to the user. The generated schema provides a ``validationErrors`` field on mutation results that can be used to provide feedback to the API consumer about invalid mutation input. The type of ``validationError`` field is a list of dictionary objects with two fields, ``path`` and ``error``. The ``path`` field indicates the path on the input data that the ``error`` applies to. The ``path`` field is handy when, for example, we're inserting a ``Label`` and a nested list of its ``Artist``\s, and our validation returns errors for the first ``Artist`` in the list. The value of ``path`` will be ``['artists', 0]``, allowing us to know where exactly the error happened. .. _schemas-validation-errors: Validation errors ----------------- To populate the ``validationErrors`` field, the ``Entity`` class's ``validator`` must be set to an object that has a single method, ``validate``, which returns a two-item tuple ``(is_valid, errors)``. Let's look at an example. .. code-block:: python :linenos: :emphasize-lines: 5,18,26 from djraphql import Entity, AllModelFields, C, R from models import Label class LabelValidator: def validate(self, validatable_input, **kwargs): errors = {} value = validatable_input.to_value() # Validate name if not value["name"]: errors["name"] = ["Name cannot be empty."] # Validate established_year if value["established_year"] < 1700: errors["established_year"] = ["Label is too old!"] is_valid = not bool(errors) return (is_valid, errors) class LabelEntity(Entity): class Meta: model = Label fields = (AllModelFields(),) access_permissions = (C, R) validator = LabelValidator() Mostly straight forward, except for the ``validatable_input`` argument. This argument is meant to provide a more convenient view of the input data. Let's test our validation in the shell. .. code-block:: bash python manage.py shell Once in the shell, execute the following commands: .. code-block:: python from graphql_api.entities import schema schema.execute("mutation InsertLabel($name: String!) { insertLabel(data: { name: $name, establishedYear: 1699 }) { success validationErrors result { id } } }", variables={"name": ""}).data # OrderedDict([('insertLabel', {'success': False, 'validationErrors': [{'path': [], 'errors': {'name': ['Name cannot be empty.'], 'established_year': ['Label is too old!']}}], 'result': None})]) The response indicates that we correctly failed validation. Our ``validationErrors`` list contains a single entry. Note that the ``path`` field is an empty list because we encountered errors at the root of our input graph. .. _schemas-validatable-input: The ``validatable_input`` parameter ################################### Rather than the raw input data, which can be overly nested and contain things like ``tag`` or ``updateStrategy``, it's often much more ergonomic to deal with an object that mirrors your model structure. To obtain such an object, we can simply call ``validatable_input.to_value()``. This will give us a no-frills object that allows us to perform intuitive validation without having to step through ``items`` lists, for example. Another convenience offered by this parameter is dealing with camel- vs. snake-casing. If your Graphene schema has ``auto_camel_case=True`` (which is the default), then you may find it unexpected that the input is provided to us in a snake-cased format. Indeed, in the above example, you'll see that we're passing ``establishedYear`` in the GraphQL mutation, but the validator receives ``established_year``. Consequently, our ``validationErrors`` list contains snake-cased keys as well. To get around this, we can pass ``camel_case_keys=True`` to the ``to_value`` method which will change the object keys to camel-case, bringing consistency to our validation logic. .. code-block:: python # In our .validate() method value = validatable_input.to_value(camel_case_keys=True) ... # Now we can use camel-casing everywhere if value["establishedYear"] < 1700: errors["establishedYear"] = ["Label is too old!"] This will also ensure the ``validationErrors`` list has camel-case keys. .. code-block:: python schema.execute("mutation InsertLabel($name: String!) { insertLabel(data: { name: $name, establishedYear: 1699 }) { success validationErrors result { id } } }", variables={"name": ""}).data # OrderedDict([('insertLabel', {'success': False, 'validationErrors': [{'path': [], 'errors': {'name': ['Name cannot be empty.'], 'establishedYear': ['Label is too old!']}}], 'result': None})]) Enums ***** DjraphQL generates GraphQL Enum types for model fields that have ``choices``.