Mutations

In part 3 we:

  • Setup and configured Django Graphene
  • Installed GraphiQL playground
  • Wrote automated tests for GraphQL queries
  • Created the types and schemas for GraphQL queries
  • Using PyTest, ran tests for the all our Todo app’s GrapQL queries
  • Introduced the GraphiQL playground

Note that all the above was using GraphQL to only access data from the postgres db.

In order to post data to the postgres db with Graphql we are going to use a process called mutations which is similar to the query processes we defined in our last post.

TDD Graphene Mutations

We’ll begin by writing a test for creating a new todo item.

Create a new file server/todo_app/tests/test_graphql_mutations.py:

touch server/todo_app/tests/test_graphql_mutations.py

Populate test_graphql_mutations.py with the following:

# server/todo_app/tests/test_graphql_mutations.py

from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json

class MutationTestCases(GraphQLTestCase):

  def test_create_todo(self):
    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    response = self.query(
      '''
      mutation createTodo($title: String!, $task: String!, $projId: Int!, $catId: Int!) {
        createTodo(title: $title, task: $task, projId: $projId, catId: $catId) {
          title
          task
          slug
          project {
            name
            slug
          }
          category {
            name
            slug
          }
        }
      }
      ''',
      variables = {
        'title': 'todo',
        'task': 'task',
        'projId': proj1.id,
        'catId':  cat1.id,
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

Todo App Mutations Schema

Next step is to update our schema adding the create todo mutation:

# server/todo_app/schema.py

# code ...

#1
# Mutations
class CreateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  slug = graphene.String()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)

  #2
  class Arguments:
    title = graphene.String()
    task = graphene.String()
    projId = graphene.Int()
    catId = graphene.Int()

  #3
  def mutate(self, info, title, task, projId, catId):
    proj = Project.objects.get(pk=projId)

    cat = Category.objects.get(pk=catId)
      
    todo = Todo.objects.create(
      title=title, 
      task=task, 
      project=proj, 
      category=cat,
    )

    return CreateTodo(
      id=todo.id,
      title=todo.title,
      task=todo.task,
      slug=todo.slug,
      project=todo.project,
      category=todo.category,
    )
#4
class Mutation(graphene.ObjectType):
  create_todo = CreateTodo.Field()

schema = Schema(query=Query, mutation=Mutation)

What is going on here?

  • #1: Define a mutation class, define the resulting output of the mutation which is the data the server sends back to the client.
  • #2: Define the arguments that the server is expecting.
  • #3: Define the mutation method that creates a new task in the database with the data the user sent. After the server returns the CreateTask class with our new data which matches the parameters in #1.
  • #4: Create a mutation class with the field to be resolved.

Your updated server/todo_app/schema.py should be as follows:

# todo-app-graphql/server/todo_app/schema.py

import graphene
from graphene import Schema
from graphene_django import DjangoObjectType
from graphql import GraphQLError
from todo_app.models import Todo, Project, Category

class TodoType(DjangoObjectType):
  class Meta:
    model = Todo

class ProjectType(DjangoObjectType):
  class Meta:
    model = Project

class CategoryType(DjangoObjectType):
  class Meta:
    model = Category

class Query(graphene.ObjectType):
  
  # todo queries
  todos = graphene.List(TodoType)
  def resolve_todos(self, info, **kwargs):
    return Todo.objects.all()

  todo = graphene.Field(
    TodoType,
    id=graphene.Int(),
  ) 
  def resolve_todo(self, info, id):
    return Todo.objects.get(pk=id)

  # project queries
  projects = graphene.List(ProjectType)
  def resolve_projects(self, info, **kwargs):
    return Project.objects.all()

  project = graphene.Field(
    ProjectType,
    id=graphene.Int(),
  )
  def resolve_project(self, info, id):
    return Project.objects.get(pk=id)

  # category queries
  categories = graphene.List(CategoryType)
  def resolve_categories(self, info, **kwargs):
    return Category.objects.all()

  category = graphene.Field(
    CategoryType,
    id=graphene.Int(),
  )
  def resolve_category(self, info, id):
    return Category.objects.get(pk=id)

# Mutations
class CreateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  slug = graphene.String()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
 
  class Arguments:
    title = graphene.String()
    task = graphene.String()
    projId = graphene.Int()
    catId = graphene.Int()
 
  def mutate(self, info, title, task, projId, catId):
    proj = Project.objects.get(pk=projId)
 
    cat = Category.objects.get(pk=catId)
       
    todo = Todo.objects.create(
      title=title, 
      task=task, 
      project=proj, 
      category=cat,
    )
 
    return CreateTodo(
      id=todo.id,
      title=todo.title,
      task=todo.task,
      slug=todo.slug,
      project=todo.project,
      category=todo.category,
    )

class Mutation(graphene.ObjectType):
  create_todo = CreateTodo.Field()
 
schema = Schema(query=Query, mutation=Mutation)

… update server/todo_proj/schema.py with our new mutation:

# server/todo_proj/schema.py

import graphene

import todo_app.schema

class Query(
  todo_app.schema.Query,
  graphene.ObjectType
):
  pass

class Mutation(
  todo_app.schema.Mutation, 
  graphene.ObjectType
):
  pass

# update the schema variable with our mutation
schema = graphene.Schema(query=Query, mutation=Mutation)

Now we can run our tests with pytest.

docker-compose run server pytest

If all goes well we should see a listing of passed tests including our new create todo mutation test.

=============================================== test session starts ===============================================
platform linux -- Python 3.8.5, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python
cachedir: .pytest_cache
django: settings: todo_proj.settings (from env)
rootdir: /var/www/server
plugins: django-4.0.0
collected 7 items                                                                                                 

todo_app/tests/test_graphql_mutations.py::MutationTestCases::test_create_todo PASSED                        
todo_app/tests/test_graphql_queries.py::QueryTestCases::test_all_todos PASSED                              
todo_app/tests/test_models.py::CategoryModelTest::test_string_representation PASSED                         
todo_app/tests/test_models.py::TodoModelTest::test_string_representation PASSED                             
todo_app/tests/test_models.py::TodoModelTest::test_todo_field PASSED                                        
todo_app/tests/test_models.py::ProjectModelTest::test_project_field PASSED                                  
todo_app/tests/test_models.py::ProjectModelTest::test_string_representation PASSED                          

If your test fails you can always run pytest in verbose mode with the -vv flag:

docker-compose run server pytest -vv

In verbose mode pytest may tell you:

/usr/local/lib/python3.8/site-packages/graphene_django/utils/testing.py:112: in assertResponseNoErrors
    self.assertEqual(resp.status_code, 200)
E   AssertionError: 400 != 200

… which is not much, other than your mutation has an error somewhere in the code. In which case you can add another level of granularity to our mutation test using assert content == to an expected result.

# server/todo_app/tests/test_graphql_mutations.py

from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json

class MutationTestCases(GraphQLTestCase):

  def test_create_todo(self):
    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    response = self.query(
      '''
      mutation createTodo($title: String!, $task: String!, $projId: Int!, $catId: Int!) {
        createTodo(title: $title, task: $task, projId: $projId, catId: $catId) {
          title
          task
          slug
          project {
            name
            slug
          }
          category {
            name
            slug
          }
        }
      }
      ''',
      variables = {
        'title': 'todo',
        'task': 'task',
        'projId': proj1.id,
        'catId':  cat1.id,
      }
    )

    expected = {
      'data': {
        'createTodo': {
          'title': 'todo',
          'task': 'task',
          'slug': 'todo',
          'project': {
            'name': 'proj1',
            'slug': 'proj1',
          },
          'category': {
            'name': 'cat1',
            'slug': 'cat1',
          }
        }
      }
    }

    content = json.loads(response.content)
    assert content ==  expected
    self.assertResponseNoErrors(response)

I will go this extra mile if I do get the AssertionError: 400 != 200 error in which case the assert content == expected proves most helpful in tracking down the problem in the verbose test result.

Complete Schema & Tests for All Crud Operations

We have the create operation for creating a todo but, now we need update and delete operations for both the schema and mutation tests which following in the same vein as our create todo mutation.

We also need to extend our schema and tests for all the crud operations for the Project and Category models.

Again we’ll start with tests. Below are tests for the crud operations of all our models:

# server/todo_app/tests/test_graphql_mutations.py

from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json

class MutationTestCases(GraphQLTestCase):

  def test_create_todo(self):
    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    response = self.query(
      '''
      mutation createTodo($title: String!, $task: String!, $projId: Int!, $catId: Int!) {
        createTodo(title: $title, task: $task, projId: $projId, catId: $catId) {
          title
          task
          slug
          project {
            name
            slug
          }
          category {
            name
            slug
          }
        }
      }
      ''',
      variables = {
        'title': 'todo',
        'task': 'task',
        'projId': proj1.id,
        'catId':  cat1.id,
      }
    )

    expected = {
      'data': {
        'createTodo': {
          'title': 'todo',
          'task': 'task',
          'slug': 'todo',
          'project': {
            'name': 'proj1',
            'slug': 'proj1',
          },
          'category': {
            'name': 'cat1',
            'slug': 'cat1',
          }
        }
      }
    }

    content = json.loads(response.content)
    assert content ==  expected
    self.assertResponseNoErrors(response)

  def test_update_todo_name(self):
    # Test for updating a todos name

    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
    todo.save()

    response = self.query(
      '''
      mutation updateTodo($id: Int!, $title: String!) {
        updateTodo(id: $id, title: $title){
          id
          slug
          title
          task
          project {
            slug
            name
          }
          category {
            slug
            name
            parent {
              slug
              name
            }
          }
        }
      }

      ''',
        variables = {
          'id': todo.id,
          'title': 'todo updated',
        }
      )

    content = json.loads(response.content)
    #assert content == {}
    self.assertResponseNoErrors(response)

  def test_update_todo_task(self):
    # Test for updating a todos task

    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
    todo.save()

    response = self.query(
        '''
        mutation updateTodo($id: Int!, $task: String!) {
          updateTodo(id: $id, task: $task){
            id
            slug
            title
            task
            project {
              slug
              name
            }
            category {
              slug
              name
              parent {
                slug
                name
              }
            }
          }
        }

        ''',
          variables = {
            'id': todo.id,
            'task': 'todo updated task',
          }
        )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_todo_project(self):
    # Test for updating a todos project

    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
    todo.save()

    proj2 = Project(name='proj2')
    proj2.save()

    response = self.query(
        '''
        mutation updateTodo($id: Int!, $projId: Int!) {
          updateTodo(id: $id, projId: $projId){
            id
            slug
            title
            task
            project {
              slug
              name
            }
            category {
              slug
              name
              parent {
                slug
                name
              }
            }
          }
        }

        ''',
          variables = {
            'id': todo.id,
            'projId': proj2.id,
          }
        )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_todo_category(self):
    # Test for updating a todos category

    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
    todo.save()

    cat2 = Category(name='cat2')
    cat2.save()

    response = self.query(
        '''
        mutation updateTodo($id: Int!, $catId: Int!) {
          updateTodo(id: $id, catId: $catId){
            id
            slug
            title
            task
            project {
              slug
              name
            }
            category {
              slug
              name
              parent {
                slug
                name
              }
            }
          }
        }

        ''',
          variables = {
            'id': todo.id,
            'catId': cat2.id,
          }
        )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_todo_is_completed(self):
    # Test for updating a todos is_completed status

    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
    todo.save()


    response = self.query(
        '''
        mutation updateTodo($id: Int!, $isCompleted: Boolean!) {
          updateTodo(id: $id, isCompleted: $isCompleted){
            id
            slug
            title
            task
            isCompleted
            project {
              slug
              name
            }
            category {
              slug
              name
              parent {
                slug
                name
              }
            }
          }
        }

        ''',
          variables = {
            'id': todo.id,
            'isCompleted': True,
          }
        )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_todo_everything(self):
    # Test for updating all of a todo's attributes at once

    proj1 = Project(name='proj1')
    proj1.save()

    cat1 = Category(name='cat1')
    cat1.save()

    proj2 = Project(name='proj2')
    proj2.save()

    cat2 = Category(name='cat2')
    cat2.save()

    todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
    todo.save()


    response = self.query(
        '''
        mutation updateTodo(
          $id: Int!,
          $title: String!,
          $task: String!,
          $projId: Int!,
          $catId: Int!,
          $isCompleted: Boolean!
          ) {
          updateTodo(
            id: $id,
            title: $title,
            task: $task,
            projId: $projId,
            catId: $catId,
            isCompleted: $isCompleted,
            ){
            id
            slug
            title
            task
            isCompleted
            project {
              slug
              name
            }
            category {
              slug
              name
              parent {
                slug
                name
              }
            }
          }
        }

        ''',
          variables = {
            'id': todo.id,
            'title': 'updated title',
            'task': 'updated task',
            'projId': proj2.id,
            'catId': cat2.id,
            'isCompleted': True,
          }
        )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_delete_todo(self):
    todo = Todo.objects.create(title='todo')

    response = self.query(
      '''
      mutation DeleteTodo($id: Int!) {
        deleteTodo(id: $id) {
          id
          title
        }
      }
      ''',
      variables = {
        'id': todo.id
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_create_project(self):
    response = self.query(
      '''
      mutation createProject($name: String!, $description: String!) {
        createProject(name: $name, description: $description) {
          id
          name
          description
          slug
        }
      }
      ''',
      variables = {
        'name': 'My Project',
        'description': 'Description for my project',
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_delete_project(self):
    project = Project.objects.create(name='foo')

    response = self.query(
      '''
      mutation deleteProject($id: Int!) {
        deleteProject(id: $id) {
          id
          name
        }
      }
      ''',
      variables = {
        'id': project.id,
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_project_name(self):
    project = Project.objects.create(name='foo')
    # Test updating both name of a project

    response = self.query(
      '''
      mutation updateProject($id: Int!, $name: String!) {
        updateProject(id:$id, name: $name) {
          id
          name
          description
          slug
        }
      }
      ''',
      variables = {
        'id': project.id,
        'name': 'My Project',
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_project_name_description(self):
    project = Project.objects.create(name='foo')
    # Test updating both name and description of a project

    response = self.query(
      '''
      mutation updateProject($id: Int!, $name: String!, $description: String!) {
        updateProject(id:$id, name: $name, description: $description) {
          id
          name
          description
          slug
        }
      }
      ''',
      variables = {
        'id': project.id,
        'name': 'My Project',
        'description': 'Description for my project',
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_project_description(self):
    # Test updating of project description
    project = Project.objects.create(name='foo', description='bar')

    response = self.query(
      '''
      mutation updateProject($id: Int!, $description: String!) {
        updateProject(id:$id, description: $description) {
          id
          name
          description
          slug
        }
      }
      ''',
      variables = {
        'id': project.id,
        'description': 'Description for my project',
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_create_category_no_parent(self):
    response = self.query(
      '''
      mutation createCategory($name: String!) {
        createCategory(name: $name) {
          id
          name
          slug
        }
      }
      ''',
      variables = {
        'name': 'my category',
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_create_category_with_parent(self):
    parent = Category.objects.create(name='Category Parent')

    response = self.query(
      '''
      mutation createCategory($name: String!, $parentId: Int!) {
        createCategory(name: $name, parentId: $parentId) {
          id
          name
          slug
        }
      }
      ''',
      variables = {
        'name': 'my category',
        'parentId': parent.id
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_delete_category(self):
    category = Category.objects.create(name='foo')

    response = self.query(
      '''
      mutation deleteCategory($id: Int!) {
        deleteCategory(id: $id) {
          id
          name
        }
      }
      ''',
      variables = {
        'id': category.id,
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_category_name(self):
    category = Category.objects.create(name='foo')
    # Test for changing a category's name

    response = self.query(
      '''
      mutation updateCategory($id: Int!,$name: String!){
        updateCategory(id: $id, name: $name) {
          id
          name
          slug
        }
      }
      ''',
      variables = {
        'id': category.id,
        'name': 'New Category Name',
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_category_parent(self):
    categoryParent = Category.objects.create(name='Parent Category')
    category = Category.objects.create(name='foo')
    # Test for moving category into a parent category.

    response = self.query(
      '''
      mutation updateCategory($id: Int!,$parentId: Int!){
        updateCategory(id: $id, parentId: $parentId) {
          id
          name
          slug
          parent {
            id
            name
            slug
          }
        }
      }
      ''',
      variables = {
        'id': category.id,
        'parentId': categoryParent.id,
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

  def test_update_category_name_parent(self):
    categoryParent = Category.objects.create(name='category parent')
    category = Category.objects.create(name='foobar')
    # Test for changing a category's name & moving renamed category into a parent category

    response = self.query(
      '''
      mutation updateCategory($id: Int!, $name: String!, $parentId: Int!){
        updateCategory(id: $id, name: $name, parentId: $parentId) {
          id
          name
          slug
          parent {
            id
            name
            slug
          }
        }
      }
      ''',
      variables = {
        'id': category.id,
        'name': 'New Category Name',
        'parentId': categoryParent.id
      }
    )

    content = json.loads(response.content)
    self.assertResponseNoErrors(response)

Running these tests will throw multiple errors which clarifies our thinking in the sense that pytest is telling us what object types, fields and resolvers we need to add to our schema.

The following is a complete update of the todo app schema which should enable us to pass all our tests:

# server/todo_app/schema.py

import graphene
from graphene import Schema
from graphene_django import DjangoObjectType
from graphql import GraphQLError
from todo_app.models import Todo, Project, Category

class TodoType(DjangoObjectType):
  class Meta:
    model = Todo

class ProjectType(DjangoObjectType):
  class Meta:
    model = Project

class CategoryType(DjangoObjectType):
  class Meta:
    model = Category

class Query(graphene.ObjectType):
  
  # todo queries
  todos = graphene.List(TodoType)
  def resolve_todos(self, info, **kwargs):
    return Todo.objects.all()

  todo = graphene.Field(
    TodoType,
    id=graphene.Int(),
  ) 
  def resolve_todo(self, info, id):
    return Todo.objects.get(pk=id)

  # project queries
  projects = graphene.List(ProjectType)
  def resolve_projects(self, info, **kwargs):
    return Project.objects.all()

  project = graphene.Field(
    ProjectType,
    id=graphene.Int(),
  )
  def resolve_project(self, info, id):
    return Project.objects.get(pk=id)

  # category queries
  categories = graphene.List(CategoryType)
  def resolve_categories(self, info, **kwargs):
    return Category.objects.all()

  category = graphene.Field(
    CategoryType,
    id=graphene.Int(),
  )
  def resolve_category(self, info, id):
    return Category.objects.get(pk=id)

# Mutations
class CreateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  slug = graphene.String()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)

  class Arguments:
    title = graphene.String()
    task = graphene.String()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, title, task, projId, catId):
    proj = Project.objects.get(pk=projId)

    cat = Category.objects.get(pk=catId)
      
    todo = Todo.objects.create(
      title=title, 
      task=task, 
      project=proj, 
      category=cat,
    )

    return CreateTodo(
      id=todo.id,
      title=todo.title,
      task=todo.task,
      slug=todo.slug,
      project=todo.project,
      category=todo.category,
    )

class DeleteTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  ok = graphene.Boolean()

  class Arguments:
    id = graphene.Int()

  def mutate(self, info, id=id):
    todo = Todo.objects.get(pk=id)

    if todo:
      todo.delete()
      return DeleteTodo(
        id = todo.id,
        title = todo.title,
        ok = True
      )

class UpdateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  slug = graphene.String()

  class Arguments:
    id = graphene.Int()
    title = graphene.String()
    task = graphene.String()
    is_completed = graphene.Boolean()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, id, title=None, task=None, is_completed=None, projId=None, catId=None):
    todo = Todo.objects.get(pk=id)
    if projId:
      project = Project.objects.get(pk=projId)
      if not project:
        raise Exception('There is no project with this id.')

    if catId:
      category = Category.objects.get(pk=catId)
      if not category:
        raise Exception('No category with this id exists.')

    if todo:
      if title:
        todo.title = title
      if task:
        todo.task = task
      if is_completed:
        todo.is_completed = is_completed
      if projId:
        todo.project = project
      if catId:
        todo.category = category

      todo.save()

      return UpdateTodo(
        id = todo.id,
        title = todo.title,
        task = todo.task,
        is_completed = todo.is_completed,
        project = todo.project,
        category = todo.category,
        slug = todo.slug,
      )
    else:
      raise Exception('There is no todo with this id.')

class CreateProject(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  description = graphene.String()
  slug = graphene.String()

  class Arguments:
    name = graphene.String()
    description = graphene.String()

  def mutate(self, info, name, description=None):
    project = Project.objects.create(name=name, description=description)

    return CreateProject(
      id = project.id,
      name = project.name,
      description = project.description,
      slug = project.slug
    )

class DeleteProject(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  ok = graphene.Boolean()

  class Arguments:
    id = graphene.Int()

  def mutate(self, info, id=id):
    project = Project.objects.get(pk=id)

    if project:
      project.delete()
      return DeleteProject(
        id = project.id,
        name = project.name,
        ok = True,
      )

class UpdateProject(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  description = graphene.String()
  slug = graphene.String()

  class Arguments:
    id = graphene.Int()
    name = graphene.String()
    description = graphene.String()
  
  def mutate(self, info, id=id, name=None, description=None):
    project = Project.objects.get(pk=id)

    if project:
      if name:
        project.name = name
      if description:
        project.description = description

      project.save()

      return UpdateProject(
        id = project.id,
        name = project.name,
        description = project.description,
        slug = project.slug, 
      )
    else:
      raise Exception('There is no project with this id.')

class CreateCategory(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  slug = graphene.String()
  parent = graphene.Field(CategoryType)

  class Arguments:
    name = graphene.String()
    parentId = graphene.Int()

  def mutate(self, info, name, parentId=None):

    if parentId:
      parent = Category.objects.get(pk=parentId)
      category = Category.objects.create(name=name, parent=parent)
    else:
      category = Category.objects.create(name=name)

    return CreateCategory(
      id=category.id,
      name=category.name,
      slug=category.slug,
      parent=category.parent,
    )

class DeleteCategory(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  slug = graphene.String()
  ok = graphene.Boolean()

  class Arguments:
    id = graphene.Int()

  def mutate(self, info, id=id):

    category = Category.objects.get(pk=id)
    if category:
      category.delete()
      return DeleteCategory(
        id = category.id,
        name = category.name,
        slug = category.slug,
        ok = True
      )

class UpdateCategory(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  parent = graphene.Field(CategoryType)
  slug = graphene.String()

  class Arguments:
    id = graphene.Int()
    name = graphene.String()
    parentId = graphene.Int()

  def mutate(self, info, id, name=None, parentId=None):
    category = Category.objects.get(pk=id)

    if parentId:
      parent = Category.objects.get(pk=parentId)
      if not parent:
        raise Exception('No category with this id to be used as the updated parent.')


    if category:
      if name:
        category.name = name
      if parentId:
        category.parent = parent
      
      category.save()

      return UpdateCategory(
        id = category.id,
        name = category.name,
        parent = category.parent,
        slug = category.slug,
      )
    else:
      raise Exception('No category with this id.')      



class Mutation(graphene.ObjectType):
  create_todo = CreateTodo.Field()
  update_todo = UpdateTodo.Field()
  delete_todo = DeleteTodo.Field()
  create_project = CreateProject.Field()
  delete_project = DeleteProject.Field()
  update_project = UpdateProject.Field()
  create_category = CreateCategory.Field()
  delete_category = DeleteCategory.Field()
  update_category = UpdateCategory.Field()

schema = Schema(query=Query, mutation=Mutation)

Running our tests…

docker-compose run server pytest

… all 30 of my tests passed. This gives me enormous confidence moving forward with this project.

Next post we add java web token, JWT, user authentication allowing only an authenticated user to create, edit and delete their project todos.