Giter Site home page Giter Site logo

strawberry-django-plus's Introduction

strawberry-django-plus

build status coverage downloads PyPI version python version django version

Warning

All the extra features provided by this lib were contributed and merged directly into the official strawberry-graphql-django lib. Since then this lib is deprecated and the official integration should be used instead and development will continue there!

If you were using this lib before, check out the migration guide for more information on how to migrate your code.

Enhanced Strawberry integration with Django.

Built on top of strawberry-django integration, enhancing its overall functionality.

Check the docs for information on how to use this lib.

Features

Installation

pip install strawberry-django-plus

Licensing

The code in this project is licensed under MIT license. See LICENSE for more information.

Stats

Recent Activity

strawberry-django-plus's People

Contributors

bellini666 avatar blueyed avatar claggierk avatar cybniv avatar dependabot[bot] avatar devkral avatar edomora97 avatar edusig avatar eloff avatar fabien-michel avatar gersmann avatar github-actions[bot] avatar janbednarik avatar kitefiko avatar lucaspickering avatar menegasse avatar mhdismail avatar moritz89 avatar mumumumu avatar nexmond avatar nrbnlulu avatar odysseyj avatar oleo65 avatar pabloalexis611 avatar parrotmac avatar patrick91 avatar pcraciunoiu avatar rubensoleao avatar sebtheiler avatar wodcz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

strawberry-django-plus's Issues

QueryOptimizer does not work when returning Node interface

Hi! First of all, congratulations with the library, it's a great addition to strawberry's ecosystem.

I'm trying to implement relay's node query:

type Query{
   node(id: ID!): Node
}

which I've implemented with:

@gql.type
class Query:
    @gql.field
    def node(info: Info, id: gql.relay.GlobalID) -> gql.Node:
        return id.resolve_node(info, required=True)

When executing a query using fragments:

{
node(id="id"){
  id
  ... on MyDjangoType{
    fieldA
    fieldB
   }
  } 
}

The query optimizer does not seem capable of optimizing it.

Strawberry 0.99.0 break this libary

update strawberry to 0.99.0 break this library

File "/home/darwin/.virtualenvs/django4/lib/python3.8/site-packages/strawberry_django_plus/relay.py", line 311, in <module> DEFAULT_SCALAR_REGISTRY[GlobalID] = ScalarDefinition( TypeError: __init__() missing 1 required positional argument: 'specified_by_url'
I happened because strawberry introduce new argument 'specified_by_url'

Interfaces failing with foreign keys

Hey again! My apologies for issue spamming, but I've come across another problem when dealing with interfaces and polymorphic models. Please consider the following example:

# models.py
class Media(Polymorphic):
    name = models.TextField()

class Movie(Media):
    director = models.TextField()

class Book(Media):
    author = models.TextField()

class Entry(models.Model):
    media = models.ForeignKey(Media)

# schema.py
@gql.django.interface(Media)
class MediaType:
    name: auto

@gql.django.type(Movie)
class MovieType(MediaType):
    name: auto
    director: auto

@gql.django.type(Book)
class BookType(MediaType):
    name: auto
    author: auto

@gql.django.type(Entry)
class EntryType:
    media: MediaType

@gql.type
class Query:
    @gql.django.field
    def get_entry(self, info: Info, id: int) -> EntryType:
        return Entry.objects.get(id=id)

So, there's a bit going on there, but the gist is that we have an entry model that has a foreign-key to a polymorphic type which can be either a movie or a book. Up to this point it works as expected, and my GraphQL schema looks correct. However, consider the following query:

query entry($id: int) {
  getEntry(id: $id) {
    media {
      __typename,
      name,
      ... on MovieType {
        director
      },
      ... on BookType {
        author
      }
    }
  }
}

This query will fail for every foreign-key that is not a movie, as it will always try to resolve the first type in the interface instead of determining the correct type of the Django model and resolving only that interface sub type.

I've done a bit of digging around and it appears that the Django GraphQL field resolvers aren't converting Django models to Strawberry types (please correct me if I'm wrong). This could be the cause of the issue, as in the Strawberry documentation it mentions that we may need to return Strawberry types in order to correctly identify the interface sub type.

How to use federation?

An example would've been nice showing how to use federation with Django models using this package.

Merge to strawberry / strawberry-django

Hello @bellini666, I am willing to help with the process of merging this repo to strawberry and i have some suggestions / questions.
1.could you provide list of the features that are divided to what to contribute to strawberry what to strawberry-django and what should stay here?
2. do you have any plan on how to do this?
3. I want to create a site for the documentation. I am familiar with mkdocs served by gh-pages what are your thoughts? Also, should they live inside the main strawberry site or should they be in sole strawberry-django site?
4. should we save the current API or prefer the strawberry-django API?
5. any other things I should know before I dive into this?

Need help on custom relay ID resolution

Hi, thank you for the amazing lib. I am implementing relay.Node for several of my django models and I was wondering if there was an easy way to replace id: GlobalID! with a custom resolver, e.g. id: str! where the id resolves to a unique string field on the model. This is so that the IDs which are being used in front-end look more neat and they don't leak the database model ids.

e.g.

# model
class Order(models.Model):
    number = models.Charfield(unique=True)

# type
@gql.django.type(Order)
class Order(relay.Node):
     id: str!
     number: gql.auto

So, instead of getting id as "T3JkZXI6MQ==" (base64 of Order:1) for the instance Order(id=1, number="2022-06-07-001"), we could get something like 2022-06-07-001.

ImportError: cannot import name 'assert_never' from 'typing_extensions'

Hello there! Just tried this library for the first time (I come from the django_strawberry one) and got an error making a simple setup:

# models.py
from django.db import models


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

class Color(models.Model):
    name = models.CharField(max_length=20)
    
# types.py
import strawberry
from strawberry_django_plus import gql
from fruits import models
from strawberry.arguments import UNSET
from typing import List, Optional


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

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

@gql.django.input(models.Fruit)
class FruitInput:
    id: gql.auto
    name: gql.auto
    color: str
    amount: gql.auto

@gql.django.input(models.Color)
class ColorInput:
    id: gql.auto
    fruits: gql.auto

@gql.django.input(models.Fruit)
class UpdateFruitInput:
    id: strawberry.ID
    name: Optional[str] = UNSET
    color: Optional[str] = UNSET
    amount: Optional[int] = UNSET

@gql.django.input(models.Fruit)
class DeleteFruitInput:
    id: strawberry.ID

@gql.django.input(models.Fruit)
class RetrieveFruitInput:
    id: strawberry.ID

# resolvers.py
from copy import deepcopy
from asgiref.sync import sync_to_async
from django.shortcuts import get_object_or_404
from strawberry.types import Info

from fruits import models
from fruits.api import types
from project.common.utils import data_to_dict


@sync_to_async
def create_fruit(
    self, info: Info, data: types.FruitInput
) -> types.Fruit:
    color = models.Color.objects.get_or_create(name=data.color)[0]
    fruit_data = data_to_dict(data)
    fruit_data.pop("color")
    fruit_obj = models.Fruit(**fruit_data, color=color)
    fruit_obj.save()
    return fruit_obj

@sync_to_async
def update_fruit(
    self, info: Info, data: types.UpdateFruitInput
) -> types.Fruit:
    fruit_data: dict = data_to_dict(data)
    fruit_obj: models.Fruit = get_object_or_404(models.Fruit, pk=int(data.id))
    if data.color:
        fruit_data["color"] = models.Color.objects.get_or_create(
            name=data.color
        )[0]
    for key, value in fruit_data.items():
        setattr(fruit_obj, key, value)
    fruit_obj.save(
        update_fields=[key for key in fruit_data.keys()] 
    )
    return fruit_obj

@sync_to_async
def delete_fruit(
    self, info: Info, data: types.DeleteFruitInput
) -> types.Fruit:
    fruit_obj: models.Fruit = get_object_or_404(
        models.Fruit,
        pk=int(data.id)
    )
    deleted_obj = deepcopy(fruit_obj)
    fruit_obj.delete()
    return deleted_obj


@sync_to_async
def fruit(
    self, info: Info, data: types.RetrieveFruitInput
) -> types.Fruit:
    return models.Fruit.objects.filter(pk=data.id).first()

# schema.py
import strawberry
from strawberry_django_plus import gql, optimizer, directives
from typing import List

from fruits.api import types, resolvers

@gql.type
class Query:
    fruits: List[types.Fruit] = gql.django.field()
    fruit: types.Fruit = gql.django.field(resolver=resolvers.fruit)

@gql.type
class Mutation:

    create_fruit: types.Fruit = strawberry.mutation(
        resolver=resolvers.create_fruit
    )
    update_fruit: types.Fruit = strawberry.mutation(
        resolver=resolvers.update_fruit
    )
    delete_fruit: types.Fruit = strawberry.mutation(
        resolver=resolvers.delete_fruit
    )

schema = strawberry.Schema(
    query=Query,
    mutation=Mutation,
    extensions=[
        optimizer.DjangoOptimizerExtension,
        directives.SchemaDirectiveExtension
    ]
)

Here you have a pip freeze:

anyio==3.5.0
asgiref==3.5.0
attrs==21.4.0
backports.cached-property==1.0.1
CacheControl==0.12.10
cachy==0.3.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.10
cleo==0.7.6
click==8.0.3
clikit==0.4.3
cryptography==36.0.1
Django==3.2
django-cors-headers==3.11.0
django-extensions==3.1.5
django-simple-history==3.0.0
graphql-core==3.1.7
gunicorn==20.1.0
h11==0.13.0
html5lib==1.1
httptools==0.3.0
idna==3.3
jeepney==0.7.1
jsonschema==3.2.0
keyring==20.0.1
lockfile==0.12.2
msgpack==1.0.3
pastel==0.2.1
pexpect==4.8.0
pkginfo==1.8.2
poetry==1.0.5
psycopg2-binary==2.9.3
ptyprocess==0.7.0
pycparser==2.21
Pygments==2.11.2
pylev==1.4.0
pyparsing==2.4.7
pyrsistent==0.14.11
python-dateutil==2.8.2
python-multipart==0.0.5
pytz==2021.3
requests==2.27.1
requests-toolbelt==0.8.0
SecretStorage==3.3.1
sentinel==0.3.0
shellingham==1.4.0
six==1.16.0
sniffio==1.2.0
sqlparse==0.4.2
starlette==0.16.0
strawberry-django-plus==1.1.2
strawberry-graphql==0.95.0
strawberry-graphql-django==0.2.5
tomlkit==0.5.11
typing_extensions==4.0.1
urllib3==1.26.8
uvicorn==0.16.0
uvloop==0.16.0
webencodings==0.5.1

Any clues on what might I be doing wrong?

Support offset based pagination

The current relay.Connection is based on cursor pagination, which works great for use cases like infinite scrolling. However, in many business applications end-users expect offset based pagination: They want to see the total number of pages, and be able to jump to a specific page. Although this comes with its own problems (it could be that a user sees the same item in two pages), it's still a very common use case. Would you consider implementing such a Connection?

@gql.django.input_mutation cannot return Interface or Union

Returning an interface or Union from a input_mutation fails with callstack:

File "/.venv/lib/python3.8/site-packages/strawberry/schema/schema.py", line 85, in __init__
    self._schema = GraphQLSchema(
  File "./venv/lib/python3.8/site-packages/graphql/type/schema.py", line 226, in __init__
    collect_referenced_types(mutation)
  File "/.venv/lib/python3.8/site-packages/graphql/type/schema.py", line 432, in collect_referenced_types
    for field in named_type.fields.values():
  File "/usr/local/lib/python3.8/functools.py", line 967, in __get__
    val = self.func(instance)
  File "/.venv/lib/python3.8/site-packages/graphql/type/definition.py", line 802, in fields
    raise cls(f"{self.name} fields cannot be resolved. {error}") from error
TypeError: Mutation fields cannot be resolved. 

I've tried to simplify the reproduction as much as possible:

from strawberry_django_plus import gql
@gql.interface
class AnInterface:
    name:str

@gql.type
class Query:
    @gql.field
    def empty(self) -> str:
        return ""

@gql.type
class Mutation:
    @gql.django.input_mutation
    def my_mutation(self) -> AnInterface:
        pass

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

Note that using strawberry mutations works fine:

from strawberry_django_plus import gql
@gql.interface
class AnInterface:
    name:str

@gql.type
class Query:
    @gql.field
    def empty(self) -> str:
        return ""

@gql.type
class Mutation:
    @strawberry.mutation #<----- Changed
    def my_mutation(self) -> AnInterface:
        pass

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

AttributeError: __provides__ when using relay.Node

Is this a bug? I checked the code in types.py:_get_fields from the stacktrace below, and I don't think it should be looking up __provides__ on the origin. Let me know if you need a sample project that can reproduce it.

  File "/code/.../types.py", line 242, in <module>
    class OrganizationType(gql.relay.Node):
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 396, in wrapper
    return _process_type(
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 281, in _process_type
    fields = list(_get_fields(django_type).values())
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 229, in _get_fields
    attr = getattr(origin, name)
AttributeError: __provides__

strawberry-django filters and order not working on relay queries ?

are strawberry-django filters and order not working on relay queries?

I'm using

@gql.django.filters.filter(models.Company)
class CompanyFilter:
    id: gql.auto
    name: gql.auto

@gql.django.ordering.order(models.Company)
class CompanyOrder:
    id: gql.auto
    name: gql.auto


@gql.django.type(models.Company, filters=CompanyFilter, order=CompanyOrder)
class Company(relay.Node):
    id: gql.auto
    name: gql.auto
    is_god: gql.auto
    child_companies: List["ChildCompany"]

@gql.type
class Query:
    company: Optional[types.Company] = relay.node()
    companies: relay.Connection[types.Company] = relay.connection()

there are no arguments filters and order on companies type

image

StrawberryDjangoField.get_queryset_as_list() got multiple values for argument 'info'

Consider a setup like this:

from typing import List

from strawberry_django_plus import gql
from strawberry.types import Info

from tenants import models

@gql.django.type(models.Tenant)
class Tenant:
    name: gql.auto

def resolve_tenants(info: Info):
    print(info.context.request)
    return models.Tenant.objects.all()

@gql.type
class Query:
    tenants: List[Tenant] = gql.django.field(resolver=resolve_tenants)

The idea here would be to e.g. root all queries in the the authenticated user or otherwise use the info arg for something. However, when executing this query:

query MyQuery {
  tenants {
    name
  }
}

This is the response:

{
  "data": null,
  "errors": [
    {
      "message": "StrawberryDjangoField.get_queryset_as_list() got multiple values for argument 'info'",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "tenants"
      ]
    }
  ]
}

It seems like info is passed as a positional arg to get_queryset_as_list, while also being in the kwargs in this case.

It's very possible that i'm doing something silly here, just taking this library out for a spin. Please let me know if there is anymore info i can provide, or if there is a better way to do this.

missing node from edges when using arguments "last" and "before"

hey @bellini666, when using arguments {last: 2, before: "..."}, one node missing from edges:

before using arguments:
image

after using arguments:
image

cursor: "YXJyYXljb25uZWN0aW9uOjE=" missing from edges,

when I remove "-1" from this code on Connection it is work as expected

  if before:
            before_type, before_parsed = from_base64(before)
            assert before_type == _connection_typename
            end = min(end, int(before_parsed) - 1)

image

Implement get_queryset like in Strawberry GraphQL Django

I'm trying to switch from Graphene to Strawberry and I like this Starwberry Django Plus package with relay implementation. I'm missing feature to set global QuerySet filter on node types. For example when I want to show only active users in API.

I have found that default resolvers resolve_model_node and reslove_model_nodes are starting resolution with QuerySet containing all objects from model._default_manager. And it seems to me like there is no way to change this behaviour.

It would be nice, it there is a way to define get_queryset method for type like in Strawberry GraphQL Django. Something like:

@gql.django.type(User)
class UserType(relay.Node):
    ...
    @classmethod
    def get_queryset(cls, queryset, info):
        return queryset.filter(is_active=True)

And when it's defined, then both node and nodes resolvers will pass QuerySet trough it.

It would allow also filtering QuerySet based on info.context to implement for example row level authorization.

I can try to implement it. As I'm not familiar with this package, I have a question if it would fit core design and concepts? And if it's straightforward as it seems or there may be some catch with stuff like query optimizer?

strawberry-django-plus / relation to relay / response type

Hi, thank you for an awesome library, it's pretty interesting to see you build it. I got two questions (slightly related), if I may ask.

1st: are CRUD mutations considered a relay only feature? because at the moment, even if the input type does not inherit from Node, the mutation expects a relay global ID.

@gql.django.input(UserModel)
class UserInput:
    id: gql.auto
    first_name: gql.auto


@strawberry.type
class Mutation:
    update_user: UserType = gql.django.update_mutation(UserInput)
mutation updateUser {
  updateUser(input:{id: "1", firstName:"test"}) {
    __typename
    ... on UserType {
      username
    }
  }
}
{
  "data": null,
  "errors": [
    {
      "message": "Expected value of type 'GlobalID!', found \"1\"; Invalid base64-encoded string: number of data characters (1) cannot be 1 more than a multiple of 4",
      "locations": [
        {
          "line": 2,
          "column": 25
        }
      ],
      "path": null
    }
  ]
}

My second question would be about the return type of mutations. If I am not mistaken, the Union Type between the Return Type and OperationInfo is a work around for the difficulty of defining a dynamic attribute name on a generic python class, right?

Have you considered adding a response attribute to the generated return type? I think that could be easier to deal with in a lot of client libraries than a union type, and it also allows to pass validation errors along with the updated object.

gql.django.connection fails to use the right get_queryset when set as a field

Problem:
if django.connection is applied on a field on a django node object the wrong get_queryset is used.

here an reduced excerpt from my project


@gql.django.type(ContentReference, filters=ContentReferenceFilter)
class ContentReferenceNode(relay.Node):
    def get_queryset(...):
       ....
@gql.django.type(Content, name="Content", filters=ContentFilter)
class ContentNode(relay.Node):
    references: relay.Connection[ContentReferenceNode] = gql.django.connection(
        filters=ContentReferenceFilter
    )

    def get_queryset(...):
       ....

the field references uses the get_queryset from ContentNode instead of ContentReferenceNode.

Typo in default querying methods for relay

Hello,

There is a typo in checking default querying methods for relay:

if not _has_own_node_resolver(cls, "resolve_connection_resolver"):
      cls.resolve_connection = types.MethodType(
          lambda *args, **kwargs: resolve_connection(
              *args,
              filter_perms=True,
              **kwargs,
          ),
          cls,
  )

Typo in "resolve_connection_resolver". I think it should be "resolve_connection"

Getting Attribute Error

Working on migrating from graphene and django over to strawberry and django. Very excited about this plugin as it looks like it really enhances the strawberry+django world. So far experience very positive, running into one issue so far.

image
image

schema is federated
image

Note

  • using sync views
  • using federation
  • This error only occurs during testing (with pytest) the live version of the query works fine

Thank you!

Enabling the Django Debug Toolbar Integration raises AttributeError for `_djdt_cursor`

I followed the instructions and to enable the DjDT integration and it raises an attribute error. I am using the current latest DjDT v3.2.4. DjDT works as expected when using its own middleware.

| Traceback (most recent call last):
|   File "/usr/local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
|     response = get_response(request)
|   File "/usr/local/lib/python3.10/site-packages/strawberry_django_plus/middlewares/debug_toolbar.py", line 127, in __call__
|     response = super().__call__(request)
|   File "/usr/local/lib/python3.10/site-packages/debug_toolbar/middleware.py", line 62, in __call__
|     panel.disable_instrumentation()
|   File "/usr/local/lib/python3.10/site-packages/debug_toolbar/panels/sql/panel.py", line 151, in disable_instrumentation
|     unwrap_cursor(connection)
|   File "/usr/local/lib/python3.10/site-packages/strawberry_django_plus/middlewares/debug_toolbar.py", line 109, in _unwrap_cursor
|     del c._djdt_cursor
|   AttributeError: _djdt_cursor

[add() / set()] got an unexpected keyword argument 'bulk'"

When I'm using set() or add() on ManyToManyRel, mutation always throws errors, when I check resolver on update_m2m there is argument 'bulk' append to function add() or set() when using ManyToManyRel, I think ManyToManyField and ManyTo ManyRel doesn't recognize bulk argument.

Using add() with a many-to-many relationship, however, will not call any save() methods (the bulk argument doesn’t exist), but rather create the relationships using QuerySet.bulk_create(). If you need to execute some custom logic when a relationship is created, listen to the m2m_changed signal, which will trigger pre_add and post_add actions.

Optimizer Doesn't Work With Repeated Items in Query Top Level

If you have the following query:

query PropertyQuery {
  properties {
    id
    shortName
    __typename
  }
  properties {
    id
    longName
  }
}

The optimizer breaks down and only optimizes the first of the top level items in that query. While this is an unlikely query to ever run in reality, this behavior seems wrong

Optimizer doesn't merge doubled up sub-queries

If you create a query like the following:

query TestQuery {
  items {
    id
    nestedItems {
      id
    }
    nestedItems {
      testField
    }
  }
}

The optimizer will only flag the second of these subqueries as needed for prefetch, creating an N+1 problem for the id field on nestedItems. The response object properly merges these sub-queries, it would make sense for the optimizer to be able to handle them as well. Obviously the example query above can easily rewritten, but in the case that the front end generates a more complicated query, that won't be possible.

A more realistic example might be:

fragment NestedItemsId on NestedItemType {
  id
}

fragment NestedItemsField on NestedItemType {
 testField
}

query TestQuery {
  items {
    id
    ...NestedItemsId
    ...NestedItemsField
  }
}

ManyToMany Relation adds extra "__" to prefetch lookup

Getting the error: Cannot find '' on Team object, 'team____users' is an invalid parameter to prefetch_related() when trying to query something like the following:

{
  turns(pagination: {limit: 1}) {
    id
    team {
      id
      users {
        id
      }
    }    
  }
}

The models are defined with the following relationships:

class Turn(Model):
    team = ForeignKey("Team", null=True)
    ...

class Team(Model):
    users = ManyToManyField("Users")
    ...

And the type definitions are as follows:

@gql.django.type(Turn)
class TurnType:
    id: strawberry.ID
    team: Optional[TeamType]

@gql.django.type(Team)
class TeamType:
    id: strawberry.ID
    users: List[UserType]

@gql.djangp.type(User)
class UserType:
    id: strawberry.ID

@strawberry.type
class Query:
    turns: List[TurnType] = gql.django.field()

There is no mypy or any other type checker installed and so do not have type hints in any of the model files. I am not sure if that will cause any issues here. The issue is definitely with the prefetch configuration with the Optimizer, since if you replace the query definition with strawberry_django.field(), the code runs fine (but without the optimizations of course).

Add ForwardRef wrapper when using _eval_type on ConnectionField

Thank you for maintaining good library!
i read your issue on strawberry to request relay implementation.
i checked your library and your interface is pretty good!

but i think i found some issue when making connection inside of node.

Versions
python 3.8.1
django 3.2.5
strawberry-django-plus 1.8
strawberry-graphql 0.109.1
strawberry-graphql-django 0.2.5

Problem Situation

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,
    )

class Color(models.Model):
    name = models.CharField(max_length=20)
@strawberry_django.type(models.Fruit)
class FruitNode(Node):
    name: str = ""

    # here : Adding connection inside of node.
    @relay.connection
    async def color(self: Fruit, info) -> List[ColorNode]:
        return await info.context.data_loaders["fruits_by_color_loader"].load(self.pk)

    @classmethod
    def resolve_node(
        cls, node_id: str, *, info: Optional[Info] = None, required: bool = False
    ):
        return FruitNode(id="asdf", name="asdf")

    @classmethod
    def resolve_nodes(
        cls, *, info: Optional[Info] = None, node_ids: Optional[Iterable[str]] = None
    ):
        return [FruitNode(id="bcd", name="asdf")]
from strawberry_django_plus.relay import connection, Connection

@strawberry.type
class Query:
    ...
    fruits_conn: Connection[FruitNode] = connection()
    ...

when i try to set this on schema, and running server, i got this error

...
File "/Users/turing-211231/Documents/MathKingAPIv2/fruits/types.py", line 56, in FruitNode
  async def color(self: Fruit, info) -> List[ColorNode]:
File "/Users/turing-211231/.pyenv/versions/3.8.1/lib/python3.8/site-packages/strawberry_django_plus/relay.py", line 1218, in connection
  f = f(resolver)
File "/Users/turing-211231/.pyenv/versions/3.8.1/lib/python3.8/site-packages/strawberry_django_plus/relay.py", line 895, in __call__
    raise TypeError(
TypeError: Connection nodes resolver needs a decoration that is a subclass of Iterable, like `Iterable[<NodeType>]`, `List[<NodeType>]`, etc

so i wrapped nodes_type with ForwardRef in relay.py and it works!

https://stackoverflow.com/questions/67500755/python-convert-type-hint-string-representation-from-docstring-to-actual-type-t

from typing import (
   ...,
   ForwardRef
)

class ConnectionField(RelayField):
    """Relay Connection field.

    Do not instantiate this directly. Instead, use `@relay.connection`

    """
    
    ...

    def __call__(self, resolver: Callable[..., Iterable[Node]]):
        ...
        # here i changed
        resolved = _eval_type(ForwardRef(nodes_type), namespace, None)
        ...

Any issue to solve this like this?
Thanks.

Django forms for mutations

This is an enhancement request for supporting django forms like django-graphene does. It would mean taking a django form, model or otherwise, and automatically generating an input type from the form and an output type which gives me the error forms, or, if successful, and a model form, the saved object.

LazyType not working with relay.Node

When I'm using LazyType and reference to 'relay.Node' subclass always throw an error,

'LazyType' object has no attribute 'resolve_connection'

Filters on root Query

It appears that when a Filter is defined on the Root Query, but not the Model Type, the filter does not register.

For example, if you do:

@strawberry_django.filters.filter(User):
class UserFilter:
    id: strawberry.ID

@gql.django.type(User)
class UserType:
    id: auto

@strawberry.type
class Query:
    users: List[UserType] = gql.django.field(filters=UserFilter)

The filter will not register. You have to add it to the gql.django.type decorator for it register. This does not mirror the strawberry_django behavior where it can be provided in either location

Extra prefetch called when using custom filters on related model

Let's say I have the following model:

class Model(models.Model):
    ...

class RelatedModel(models.Model):
    original_model = ForeignKey("app.Model", related_name="related_models")

If I implement a type like:

@gql.django.type(Model)
class ModelType:
    id: strawberry.ID
    
    @gql.django.field
    async def related_models(self, info):
          return self.related_models.active()

If the optimizer if enabled, this will perform an extra prefetch on the RelatedModel, that will then be overwritten by the self.related_models.active() call, meaning an unnecessary trip to the DB.

I would propose we add an option to the OptimizerConfig to disable certain fields, selects, and prefetches. Something in the form of:

class OptimizerConfig:
    ...
    disabled_only: List[str] = dataclasses.field(default_factory=list)
    disabled_select_related: List[str] = dataclasses.field(default_factory=list)
    disabled_prefetch_related: List[PrefetchType] = dataclasses.field(default_factory=list)

This would allow the definition of a new type to be in the form:

@gql.django.type(Model, disabled_prefetches=["related_models")
class ModelType:
   ...

duplicate after cursor on edges

"after cursor" is still included in the results of edges, as far as I know "after cursor" is no longer included in the results of edges.

2022-02-07_17-13

StrawberryDjangoConnectionField ignores source

@gql.django.type(models.Color)
class ColorNode(gql.relay.Node):
    name: gql.auto
    test: gql.auto

    # This works fine and respects source
    fruits: gql.relay.Connection["FruitNode"]
   
    # This does not 
    fruits: gql.relay.Connection["FruitNode"] = gql.django.connection()

From what I can tell using gql.django.connection translates into field not having base_resolver in:

if self.base_resolver is not None:

And that causes default manager of source's model to be used for nested field:

nodes = self.model._default_manager.all()

But I might be wrong there.

Union type <type1><type2>...<typeN> can only include <type1> once.

Given I have the following types:

@gql.django.type(lm.Property)
class AgentPropertyType:
    client: str
    property_type: str
    description: str
    address: str


class FranchiseePropertyType(AgentPropertyType):
    agent: str


class FranchiserPropertyType(FranchiseePropertyType):
    franchise: str

I was wanting to return this types in the query resolver:

    properties: List[lt.AgentPropertyType | lt.FranchiseePropertyType | lt.FranchiserPropertyType] = strawberry.field(resolver=lr.properties)

However I got the following error:
image

Input field Optional[OneToManyInput] is a required field in the resulting schema

Not sure if this is expected behaviour, but if I define an input type like this:

@gql.django.input(ServiceInstance)
class ServiceInstancePartialInput(NodeInput):
    id: Optional[gql.ID]
    service: Optional[OneToManyInput]

The resulting schema marks 'service' as required field:
image

I can pass an empty object into service, since set is not required, but I'd expect service to be not required instead.
Edit: the schema allows for it, but if I do that the mutation fails.

Filter on connection causes error "lookup was already seen with a different queryset"

I have types and schema like:

@gql.django.type(GroupMembership)
class GroupMembershipType(relay.Node):
    group: "GroupType"
    is_auto: gql.auto
    ...

@gql.django.filter(Group, lookups=True)
class GroupFilter:
    name: gql.auto

@gql.django.type(Group, filters=GroupFilter)
class GroupType(relay.Node):
    name: gql.auto
    groupmembership: Optional[List[GroupMembershipType]]
    ...

@strawberry.type
class Query:
    all_groups: relay.Connection[GroupType] = gql.django.connection()

schema = strawberry.Schema(query=Query, extensions=[DjangoOptimizerExtension])

When I run query like this everything is fine:

{
  allGroups {
    edges {
      node {
        name
        groupmembership {
          isAuto
        }
      }
    }
  }
}

But when I use filters on allGroups like allGroups (filters: {name: {startsWith: "foo"}}) it returns error:

{
  "data": null,
  "errors": [
    {
      "message": "'groupmembership_set' lookup was already seen with a different queryset. You may need to adjust the ordering of your lookups.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "allGroups"
      ]
    }
  ]
}

Versions:

strawberry-django-plus==1.10.2
strawberry-graphql==0.111.2
strawberry-graphql-django==0.2.5

ImportError: cannot import name '_Unset' from 'strawberry.unset'

With strawberry 0.109.0, likely due to strawberry-graphql/strawberry#1813:

  File "…/project/app/base/models.py", line 10, in <module>
    from strawberry_django_plus import gql
  File "…/project/.venv/lib/python3.10/site-packages/strawberry_django_plus/gql/__init__.py", line 47, in <module>
    from . import django
  File "…/project/.venv/lib/python3.10/site-packages/strawberry_django_plus/gql/django.py", line 16, in <module>
    from strawberry_django_plus.filters import filter
  File "…/project/.venv/lib/python3.10/site-packages/strawberry_django_plus/filters.py", line 14, in <module>
    from .type import input
  File "…/project/.venv/lib/python3.10/site-packages/strawberry_django_plus/type.py", line 28, in <module>
    from strawberry.unset import _Unset
ImportError: cannot import name '_Unset' from 'strawberry.unset' (…/project/.venv/lib/python3.10/site-packages/strawberry/unset.py)

Query optimizer extension in docs does not work

So I tried adding the query optimizer using the instructions in the docs:

import strawberry
from strawberry_django_plus import gql
from strawberry_django_plus.optimizer import DjangoOptimizerExtension

schema = strawberry.Schema(query=Query, mutation=Mutation, 
    extension=[DjangoOptimizerExtension,]
    )

And I got the following error:
TypeError: Schema.__init__() got an unexpected keyword argument 'extension'

Also tried changing

schema = strawberry_django_plus.Schema(query=Query, mutation=Mutation, 
    extension=[DjangoOptimizerExtension,]
    )

and still got the same error:
AttributeError: module 'strawberry_django_plus' has no attribute 'Schema'

Add fields on a relay connection?

Is there a way to customize the relay connection type to add additional fields?

For example in my graphene schema I have a custom Connection subclass that adds a completed_count field (similar to total_count, but for a filtered subset of the nodes with is_cancelled=False)

This is possible with strawberry itself, but I'm not sure how to do it when using relay.connection from this repo.

Django Graphene code:

 class HqOrganizationConnection(relay.Connection):
     class Meta:
         node = OrganizationType

     total_count = graphene.Int()
     completed_count = graphene.Int()

     class Arguments:
         getHqOrganizationRequest = GetHqOrganizationRequest()

     def resolve_total_count(root, info):
         return root.iterable.count()

     def resolve_completed_count(root, info):
         return root.iterable.filter(is_cancelled=False).count()

Ask for feature: pass down the `kwargs` to the `check_condition` method

The Problem

As many devs who moved on from the graphene based libs, I found this lib provides me with what I want.
But one thing I miss, I cannot modify or check the input type before it goes to be executed in the Django ORM.

The simple solution

the implemented directive here is just amazing. To check whether a user owns an object, I subclassed the ConditionDiretive:

@dataclasses.dataclass
class OwnsObjPerm(ConditionDirective):

    message: Private[str] = "You don't have such object."

    def check_condition(
        self, root: Any, info: GraphQLResolveInfo, user: UserType, **kwargs
    ):
        pk = info.variable_values["pk"]  # get object `pk`
        if models.Evaluation.objects.filter(pk=pk, user=user).exists():
            return True

        return False

it worked. but the dict info.variable_values contains only the variables that have been sent through "variables" POST. This means, variables hard coded in the query cannot be accessed.
another problem, changing a value in info.variable_values won't affect the queryset execution.

to solve those problems I passed the kwargs from resolve of the directive down to the check_condition method as the above snippet.
I added kwargs after this line

So wonder if we can do this modification.

Alternative Solution

or we can extend the idea of directives and create a custom directive, like

@dataclasses.dataclass
class CustomDirective(SchemaDirectiveWithResolver):
    """Base auth directive definition."""

    has_resolver: ClassVar = True

    def resolve(
        self,
        helper: SchemaDirectiveHelper,
        _next: Callable,
        root: Any,
        info: GraphQLResolveInfo,
        *args,
        **kwargs,
    ):
        print(kwargs)
        resolver = functools.partial(_next, root, info, *args, **kwargs)


        if callable(resolver):
            resolver = resolver()
        return resolver

but for sure I broke down the async stuff.

so is it possible to implement an easy-to-use custom directive class, that devs can just subclass it out of box?

Have a relay.connection field on a django.type?

I think this should be allowed? We had a similar schema with graphene:

@gql.django.type(SkillModule)
class SkillModuleType(gql.relay.Node):
    id: gql.relay.GlobalID
    order: int
    name: str
    description: str
    
    @gql.relay.connection
    def active_skill_list(self: SkillModule) -> List["SkillType"]:
        return self.skill_set.filter(is_active=True)

    @gql.relay.connection
    def published_skill_list(self: SkillModule) -> List["SkillType"]:
        return self.skill_set.filter(is_published=True, is_active=True)
  File "/code/.../types.py", line 48, in <module>
    class SkillModuleType(gql.relay.Node):
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 396, in wrapper
    return _process_type(
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 281, in _process_type
    fields = list(_get_fields(django_type).values())
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 233, in _get_fields
    fields[name] = _from_django_type(django_type, name)
  File "/usr/local/lib/python3.9/site-packages/strawberry_django_plus/type.py", line 173, in _from_django_type
    elif field.django_name or field.is_auto:
AttributeError: 'ConnectionField' object has no attribute 'django_name'

Optimizer incompatible with Django Polymorphic

Hi there, thanks for your great work with strawberry-django-plus, it's very much appreciated!

I've encountered an issue with the optimize function when used in conjunction with django-polymporphic models. As an example, consider this (very) contrived example:

class Media(Polymorphic):
    name = models.TextField()

class Movie(Media):
    director = models.TextField()

class Entry(models.Model):
    media = models.ForeignKey(Media)


Entry.objects.create(
    media=Movie.objects.create(name='Star Wars', director='George Lucas'),
)

entry_a = Entry.objects.first()
entry_b = optimize(Entry.objects).first()

With django-polymorphic, foreign keys are resolved to the correct polymorphic model type when fetched. So, entry_a.media will be of type Movie. However, when using optimize, this does not happen, and entry_b.media is of type Media. This of course causes interface based queries in GraphQL to fail due to trying to extract subclass fields from the base class.

strawberry_django_plus.filters.filter makes fields required by default?!

When using strawberry_django_plus.filters.filter instead of strawberry_django.filter, the annotated fields appear to be required, i.e. activated by default in GraphiQL, and using deprecation_reason causes the schema to validate:

❌ Required input field FooFilter.bar_status cannot be deprecated.

@filter(models.Foo)
class FooFilter:
    bar_status: str = gql.django.field(deprecation_reason="use … instead")

I've seen that | None can be used with the annotation to make it optional, but I think it should be optional by default.

code ref:

@__dataclass_transform__(
order_default=True,
field_descriptors=(
StrawberryField,
_field,
node,
connection,
field.field,
field.node,
field.connection,
),
)
def filter( # noqa:A001
model: Type[Model],
*,
name: Optional[str] = None,
description: Optional[str] = None,
directives: Optional[Sequence[object]] = (),
lookups: bool = False,
) -> Callable[[_T], _T]:
return input(
model,
name=name,
description=description,
directives=directives,
is_filter="lookups" if lookups else True,
partial=True,
)

Context is `None` when using permission directives

Hi there! Thanks for putting so much effort into a great set of additional utilities for Django and Strawberry. I've encountered an issue whereby when using the IsAuthenticated permission directive results in the following exception:

  File "/usr/local/lib/python3.9/site-packages/graphql/execution/execute.py", line 625, in await_result
    return_type, field_nodes, info, path, await result
  File "/code/strawberry/strawberry/extensions/directives.py", line 19, in resolve
    result = await await_maybe(_next(root, info, *args, **kwargs))
  File "/code/strawberry-django-plus/strawberry_django_plus/optimizer.py", line 506, in resolve
    ret = _next(root, info, *args, **kwargs)
  File "/code/strawberry-django-plus/strawberry_django_plus/directives.py", line 175, in resolve
    return _next(root, info, *args, **kwargs)
  File "/code/strawberry-django-plus/strawberry_django_plus/permissions.py", line 234, in resolve
    user = cast(UserType, context.request.user)
AttributeError: 'NoneType' object has no attribute 'request'

Breakpointing in to the code shows that context is None, which seems like it should never happen.

I don't believe my setup is anything unusual; I'm running latest versions of strawberry, strawberry-graphql-django, and strawberry-django-plus; I've created a very basic schema; I'm testing using the included GraphiQL interface. Without the permission directive everything works as expected, including using the context elsewhere in my code. However, when I include the permission directive, it fails.

Thanks!

Add a way to specify fields to always load with the optimizer

Currently, the optimizer knows to always load the pk of an object. It would be nice to be able to specify other fields that need to be loaded every time.

This would be useful because I use a lot of proxy models that load the appropriate class as part of the base model __init__. These methods rely on certain fields, and if they are not part of the optimized request, it causes an N + 1 issue.

I image that the style would read something like:

@gql.django.type(MyModel, always=["proxy_model_type"])
class MyModelType:
    ...

This would cause the optimizer to always load proxy_model_type as part of its optimization

nested connection not working ?

Hi, I love this package, but when I play around and tried clone test models on this repo and tried to query this:

{ projectConn { edges { cursor node { id name milestonesConn { edges { cursor node { id name } } } } } } }

results errors

{ "data": null, "errors": [ { "message": "'Project' object has no attribute 'milestones_conn'", "locations": [ { "line": 8, "column": 9 } ], "path": [ "projectConn", "edges", 0, "node", "milestonesConn" ] } ] }

Typing error on directives using permissions

when I'm using IsAuthenticated() or HasPerm() my pycharm ide gives a warning

image

when I dig, directives only receive StrawberrySchemaDirective but permissions based on SchemaDirectiveResolver, maybe we can Union[SchemaDirectiveResolver, StrawberrySchemaDirective]

Error when using relay module without Django

I'm interested in using the relay module in a non-Django project (keeping my eyes on how strawberry-graphql/strawberry#1573 progresses).

I copied the relay and aio models out of this package and tried running your example on strawberry-graphql/strawberry#157.

# schema.py
from typing import Iterable, Optional

import strawberry
from strawberry.types.info import Info

from .utils import relay

fruits = [
    {
        "id": 1,
        "name": "Banana",
        "description": "Lorem ipsum",
    },
    {
        "id": 2,
        "name": "Apple",
        "description": None,
    },
    {
        "id": 3,
        "name": "Orange",
        "description": "Lorem ipsum",
    },
]


@strawberry.type
class Fruit(relay.Node):
    name: str
    description: Optional[str]

    @classmethod
    def resolve_node(
        cls,
        node_id: str,
        *,
        info: Optional[Info] = None,
        required: bool = False,
    ):
        for fruit in fruits:
            if str(fruit["id"]) == node_id:
                return Fruit(**fruit)

        if required:
            raise ValueError(f"Fruit by id {node_id} not found.")

        return None

    @classmethod
    def resolve_nodes(
        cls,
        *,
        info: Optional[Info] = None,
        node_ids: Optional[Iterable[str]] = None,
    ):
        node_ids = node_ids and set(node_ids)

        for fruit in fruits:
            if node_ids is not None and str(fruit["id"]) not in node_ids:
                continue

            yield Fruit(**fruit)


@strawberry.type
class Query:
    fruit: Fruit = relay.node()
    fruits_conn: relay.Connection[Fruit] = relay.connection()

    @relay.connection
    def fruits_conn_with_filter(self, name_startswith: str) -> Iterable[Fruit]:
        for fruit in fruits:
            if fruit["name"].startswith(name_startswith):
                yield Fruit(**fruit)


@strawberry.type
class Mutation:
    @relay.input_mutation
    def create_fruit(self, name: str, description: Optional[str]) -> Fruit:
        fruit_data = {
            "id": max(f["id"] for f in fruits) + 1,
            "name": name,
            "description": description,
        }
        fruits.append(fruit_data)
        return Fruit(**fruit_data)


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

When I issue the following query for Fruit 1, I get an error:

{
    fruit(id: "RnJ1aXQ6MQ==") {
        id
        name
    }
}
  File "./utils/relay.py", line 783, in get_result
    return gid.resolve_node(info)
  File "./utils/relay.py", line 293, in resolve_node
    node = n_type.resolve_node(
  File "./schema.py", line 42, in resolve_node
    return Fruit(**fruit)
TypeError: __init__() got an unexpected keyword argument 'id'

I'm unsure how this is supposed to work, given that id is a classmethod. Can I get a hand debugging this please?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.