strawberry-graphql / strawberry-django

Strawberry GraphQL Django extension
https://strawberry.rocks/docs/django
MIT License
404 stars 117 forks source link

TypeError: Mutation fields cannot be resolved. 'DjangoUpdateMutation' object has no attribute 'is_relation' #87

Closed Lucasmiguelmac closed 3 months ago

Lucasmiguelmac commented 2 years ago

Hello there!

I was basically trying this library for the first time, following [this example] incrementally with some minor tweaks like a TextChoices field for the name field in the Color model.

I could succesfully implement the following mutations:

But when I try to create the updateFruit, my Django manage.py runserver will raise the following error in the logs when adding the updateFruit under the Mutation type:

TypeError: Mutation fields cannot be resolved. 'DjangoUpdateMutation' object has no attribute 'is_relation'

As you can imagine I can also trigger this traceback by running python manage.py check I share a more detailed traceback at the end of this comment.

My models.py looks like this:

from django.db import models

class Fruit(models.Model):
    name    = models.CharField(max_length=20)
    color   = models.ForeignKey('Color', blank=True, null=True, related_name='fruits', on_delete=models.CASCADE)
    amount  = models.IntegerField()

class Color(models.Model):

    class ColorChoices(models.TextChoices):
        RED     = "Rojo"
        YELLOW  = "Amarillo"
        BLUE    = "Azul"
        ORANGE  = "Naranja"
        PURPLE  = "Violeta"
        GREEN   = "Verde"

    name = models.CharField(choices=ColorChoices.choices, max_length=20)

    def __str__(self) -> str:
        return f"{self.name}-{self.pk}"

My types.py looks like this:

import strawberry
from enum import Enum
from strawberry.django import auto
from typing import List
import strawberry_django

from fruits import models

@strawberry.enum
class ColorName(Enum):
    RED     = "Rojo"
    YELLOW  = "Amarillo"
    BLUE    = "Azul"
    ORANGE  = "Naranja"
    PURPLE  = "Violeta"
    GREEN   = "Verde"

@strawberry.django.type(models.Fruit)
class Fruit:
    id: auto
    name: auto
    color: 'Color'
    amount: auto

@strawberry.django.type(models.Color)
class Color:
    id: auto
    name: auto
    fruits: List[Fruit]

@strawberry_django.input(models.Color)
class ColorInput:
    id: auto
    name: auto
    fruits: auto

@strawberry_django.input(models.Fruit)
class FruitInput:
    id: auto
    name: auto
    amount: auto
    color: auto

@strawberry_django.input(models.Fruit, partial=True)
class FruitPartialInput:
    pass

My schema.py looks like this:

import strawberry
from typing import List
from strawberry_django import mutations

from fruits.types import Fruit, FruitInput, Color, ColorInput, FruitPartialInput

@strawberry.type
class Query:
    fruits: List[Fruit] = strawberry.django.field()
    colors: List[Color] = strawberry.django.field()

@strawberry.type
class Mutation:
    createColor: Color = mutations.create(ColorInput)
    createFruit: Fruit = mutations.create(FruitInput)
    updateFruit: Fruit = mutations.update(FruitPartialInput)

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

Though probably irrelevant, my urls.py looks like this:

from django.conf import settings
from django.contrib import admin
from django.urls import path

from strawberry.django.views import AsyncGraphQLView
from project.schema import schema

if settings.DEBUG:
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('graphql/', AsyncGraphQLView.as_view(schema=schema)),
    ]
else:
    urlpatterns = [
        # Safe urls grabbed from .env
    ]

My settings.py looks like this:

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ.get('SECRET_KEY')

DEBUG = os.environ.get('DEBUG')

ALLOWED_HOSTS = [el for el in os.environ.get("ALLOWED_HOSTS").split(",")]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Project apps
    'user',
    'fruits',

    # Third party apps
    'corsheaders',
    'strawberry.django',
    'django_extensions'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware'
]

ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'

DATABASES = {
    'default': {
        'ENGINE':os.environ.get("SQL_ENGINE"),
        'NAME': os.environ.get('SQL_DATABASE'),
        'USER': os.environ.get('SQL_USER'),
        'PASSWORD': os.environ.get('SQL_PASSWORD'),
        'HOST': os.environ.get('SQL_HOST'),
        'PORT': os.environ.get('SQL_PORT'),
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

STATIC_URL = '/static/'

AUTH_USER_MODEL = 'user.User'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Detailed traceback:

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/graphql/type/definition.py", line 735, in fields
    fields = resolve_thunk(self._fields)
  File "/usr/local/lib/python3.10/site-packages/graphql/type/definition.py", line 264, in resolve_thunk
    return thunk() if callable(thunk) else thunk
  File "/usr/local/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 280, in get_graphql_fields
    graphql_fields[field_name] = self.from_field(field)
  File "/usr/local/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 146, in from_field
    for argument in field.arguments:
  File "/usr/local/lib/python3.10/site-packages/strawberry_django/mutations/fields.py", line 38, in arguments
    return arguments + super().arguments
  File "/usr/local/lib/python3.10/site-packages/strawberry_django/filters.py", line 148, in arguments
    if self.is_relation is False:
AttributeError: 'DjangoUpdateMutation' object has no attribute 'is_relation'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/workspace/manage.py", line 22, in <module>
    main()
  File "/workspace/manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.10/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 398, in execute
    output = self.handle(*args, **options)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/commands/check.py", line 63, in handle
    self.check(
  File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 419, in check
    all_issues = checks.run_checks(
  File "/usr/local/lib/python3.10/site-packages/django/core/checks/registry.py", line 76, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
  File "/usr/local/lib/python3.10/site-packages/django/core/checks/urls.py", line 13, in check_url_config
    return check_resolver(resolver)
  File "/usr/local/lib/python3.10/site-packages/django/core/checks/urls.py", line 23, in check_resolver
    return check_method()
  File "/usr/local/lib/python3.10/site-packages/django/urls/resolvers.py", line 412, in check
    for pattern in self.url_patterns:
  File "/usr/local/lib/python3.10/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/usr/local/lib/python3.10/site-packages/django/urls/resolvers.py", line 598, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File "/usr/local/lib/python3.10/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/usr/local/lib/python3.10/site-packages/django/urls/resolvers.py", line 591, in urlconf_module
    return import_module(self.urlconf_name)
  File "/usr/local/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/workspace/project/urls.py", line 6, in <module>
    from project.schema import schema
  File "/workspace/project/schema.py", line 19, in <module>
    schema = strawberry.Schema(query=Query, mutation=Mutation)
  File "/usr/local/lib/python3.10/site-packages/strawberry/schema/schema.py", line 84, in __init__
    self._schema = GraphQLSchema(
  File "/usr/local/lib/python3.10/site-packages/graphql/type/schema.py", line 210, in __init__
    collect_referenced_types(mutation)
  File "/usr/local/lib/python3.10/site-packages/graphql/type/schema.py", line 422, in collect_referenced_types
    for field in named_type.fields.values():
  File "/usr/local/lib/python3.10/functools.py", line 981, in __get__
    val = self.func(instance)
  File "/usr/local/lib/python3.10/site-packages/graphql/type/definition.py", line 737, in fields
    raise TypeError(f"{self.name} fields cannot be resolved. {error}")
TypeError: Mutation fields cannot be resolved. 'DjangoUpdateMutation' object has no attribute 'is_relation'
root@73aabd78fd1f:/workspace# 

Upvote & Fund

Fund with Polar

Lucasmiguelmac commented 2 years ago

Update: same error raised when trying to implement the following mutation under my Mutation class:

deleteFruit: Fruit = mutations.delete()
gersmann commented 2 years ago

Got the same issue, in a pretty plain vanilla setup, InputType with one string field.

Django 3.2 / Python 3.8

Desttro commented 2 years ago

Same issue for me, using Django 4.0.2 and Python 3.10.2. It works, when you use a List type:

update_fruit: List[FruitType] = mutations.update(FruitPartialInput)

But for mutations.delete() does not work.

la4de commented 2 years ago

Both update and delete mutations require List type. See example project. https://github.com/strawberry-graphql/strawberry-graphql-django/blob/main/examples/django/app/schema.py#L39-L40

Following should work. The reason for that is that both of these mutations may update or delete multiple items and therefore multiple items may be returned.

@strawberry.type
class Mutation:
    createColor: Color = mutations.create(ColorInput)
    createFruit: Fruit = mutations.create(FruitInput)
    updateFruits: List[Fruit] = mutations.update(FruitPartialInput)
    deleteFruits: List[Fruit] = mutations.delete()

We definitely have to improve error reporting.

gersmann commented 2 years ago

@la4de thanks for the feedback. I am using the django-strawberry-plus package for crud mutations now, but it feels strange to have only the possibility to do 'update many' mutations. What is the selection predicate for those mutations?

la4de commented 2 years ago

@gersmann, that's very good question. You need to add filters for that purpose. See filtering chapter from our docs https://github.com/strawberry-graphql/strawberry-graphql-django/blob/main/docs/references/mutations.md

After that you can update data for filtered set.

{
  updateFruits(data: { name: "orange" }, filters: { id: { inList: [1, 2] } } ) {
    id
    name
  }
}
cpontvieux-systra commented 1 year ago

Be careful, the filter is applied to both the query to update and the query for return results.

So that:

{
  updateFruits(data: { name: "orange" }, filters: { name: { exact: "Orange" } } ) {
    id
    name
  }
}

Will promptly rename the "Orange" fruit(s) to "orange" fruit(s) but will return you 0 result.

This is because the filter "name is exactly Orange" will match no model instance once the migration has been applied.

This is probably a bug, but I don’t see how to fix this right now.

bellini666 commented 3 months ago

The mutations update has been tweaked a lot since this issue was reported, and I'm almost sure this issue has been fixed already.

Going to mark it as resolved, but please comment in here if that is not the case so that we can reopen it :)