Strawberry-django-plus - Enhanced Strawberry GraphQL integration with Django

Overview

strawberry-django-plus

build status coverage PyPI version python version django version

Enhanced Strawberry integration with Django.

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

Features

  • All of supported features by strawberry-django.
  • Extension that automatically optimize queries (using only/select_related/prefetch_related) to solve graphql N+1 problems, with support for fragment spread, inline fragments, @include/@skip directives, prefetch merging, etc
  • Improved sync/async resolver that priorizes the model's cache to avoid have to use sync_to_async when not needed.
  • Support for Django choices enums using (requires django-choices-field)
  • Relay support for queries, connections and input mutations.
  • Django Debug Toolbar integration with graphiql to display metrics like SQL queries
  • (Coming Soon...) Improved Django mutations with automatic validation errors integration.
  • (Coming Soon...)Integration with Django Guardian for per-object permission management.
  • A well typed and documented API.

Installation

Install it with pip:

pip install strawberry-django-plus

Introduction

Since this lib has a long name, it does provide a shortcut called gql where all of strawberry's API and ours can be accessed.

from strawberry_django_plus import gql

# All strawberry's base api can be found directly on gql, like:
gql.type  # same as strawberry.type
gql.field  # same as strawberry.field
...

# The strawberry-django API and our custom implementation can be found on gql.django, like:
gql.django.type
gql.django.field
...

# We also have a custom relay implementation in here:
gql.relay

How To

Automatic QuerySet Optimization

The automatic optimization is enabled by adding the DjangoOptimizerExtension to your strawberry's schema config.

import strawberry
from strawberry_django_plus.optimizer import DjangoOptimizerExtension

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

Now consider the following:

# models.py

class Artist(models.Model):
    name = models.CharField()

class Album(models.Moodel):
    name = models.CharField()
    release_date = models.DateTimeField()
    artist = models.ForeignKey("Artist", related_name="albuns")

class Song(models.Model):
    name = model.CharField()
    duration = models.DecimalField()
    album = models.ForeignKey("Album", related_name="songs")

# schema.py
from strawberry_django_plus import gql

@gql.django.type(Artist)
class ArtistType:
    name: auto
    albums: "List[AlbumType]"

@gql.django.type(Album)
class AlbumType:
    name: auto
    release_date: auto
    artist: ArtistType
    songs: "List[SongType]"

@gql.django.type(Song)
class SongType:
    name: auto
    duration: auto
    album_type: AlbumType

@gql.type
class Query:
    artist: Artist = gql.django.field()
    songs: List[SongType] = gql.django.field()

Querying the artist field:

{
  artist {
    id
    name
    albums {
      id
      name
      songs {
        id
        name
      }
    }
  }
}
# This will generate a query like:
Artist.objects.all().only("id", "name").prefetch_related(
    Prefetch(
        "albums",
        queryset=Album.objects.all().only("id", "name").prefetch_related(
            "songs",
            Song.objects.all().only("id", "name"),
        )
    ),
)

And querying the songs list:

{
  song {
    id
    album
    id
    name
    artist {
      id
      name
      albums {
        id
        name
        release_date
      }
    }
  }
}
# This will generate a query like:
Song.objects.all().only(
    "id",
    "album",
    "album__id",
    "album__name",
    "album__release_date",  # Note about this below
    "album__artist",
    "album__artist__id",
).select_related(
    "album",
    "album__artist",
).prefetch_related(
    "album__artist__albums",
    Prefetch(
        "albums",
        Album.objects.all().only("id", "name", "release_date"),
    )
)

Note that even though album__release_date field was not selected here, it got selected in the prefetch query later. Since Django caches known objects, we have to select it here or else it would trigger extra queries latter.

It is also possible to include hints for non-model fields using the field api or even our @model_property (or its cached variation, @cached_model_property) decorator on the model itself, for people who likes to keep all the business logic at the model.

For example, the following will automatically optimize only and select_related if that field gets selected:

from strawberry_django_plus import gql

class Song(models.Model):
    name = models.CharField()

    @gql.model_property(only=["name", "album__name"], select_related=["album"])
    def name_with_album(self) -> List[str]:
        return f"{self.album.name}: {self.name}"

@gql.django.type(Song)
class SongType:
    name: auto
    name_with_album: str

Another option would be to define that on the field itself:

@gql.django.type(Song)
class SongType:
    name: auto
    name_with_album: str = gql.django.field(
        only=["name", "album__name"],
        select_related=["album"],
    )

Django Choices Enums

Convert choices fields into GraphQL enums by using Django Choices Field extension.

from django_choices_field import TexChoicesField

class Song(models.Model):
    class Genre(models.TextChoices):
        ROCK = "rock", "Rock'n'Roll"
        METAL = "metal", "Metal"
        OTHERS = "others", "Who Cares?"

    genre = TextChoicesField(choices_enum=Genre)

In that example, a new enum called Genre will be created and be used for queries and mutations.

If you want to name it differently, decorate the class with @gql.enum with your preferred name so that strawberry-django-plus will not try to register it again.

Relay Support

We have a custom relay spec implementation. It is not tied to Django at all to allow its usage with other types.

It provides types and fields for node and connection querying. For example:

# schema.py
from strawberry_django_plus import gql
from strawberry_django_plus.gql import relay

@gql.type
class Fruit(relay.Node):
    name: str

    def resolve_node(cls, node_id, info, required=False):
        ...

    def resolve_nodes(cls, node_id, info, node_ids=False):
        ...


@gql.type
class Query:
    fruit: Optional[Fruit] = relay.node()
    fruits_connection: relay.Connection[Fruit] = relay.connection()

    @relay.connection
    def fruits_connection_filtered(self, name_startswith: str) -> Iterable[Fruit]:
        # Note that this resolver is special. It should not resolve the connection, but
        # the iterable of nodes itself. Thus, any arguments defined here will be appended
        # to the query, and the pagination of the iterable returned here will be
        # automatically handled.
        ...

This will generate a schema like this:

interface Node {
  id: GlobalID!
}

type Fruit implements Node {
  id: GlobalID!
  name: String!
}

type FruitEdge implements Node {
  cursor: String!
  node: Fruit
}

type FruitConnection {
  edges: [ShipEdge!]!
  pageInfo: PageInfo!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  fruit(id: GlobalID!): Fruit
  fruits_connection(before: String, after: String, first: Int, last: Int): FruitConnection
  fruits_connection_filtered(
      before: String,
      after: String,
      first: Int,
      last: Int,
      nameStartswith: String!
  ): FruitConnection
}

It is expected that types implementing the Node interface define some methods, like resolve_nodes and resolve_node. Take a look at the documentation for more information.

Also note that Django fields created with @gql.django.type automatically implements all of the required methods when the type inherits from Node.

This module also exposes a mutation that converts all of its arguments to a single input. For example:

@gql.type
class Mutation:
    @relay.input_mutation
    def create_fruit(name: str) -> Fruit:
        ....

Will generate those types:

input CreateFruitInput {
  name: String!
}

type Mutation {
  createFruit(input: CreateFruitInput!): Fruit
}

Django Debug Toolbar Integration

Install Django Debug Toolbar and change its middleware from:

MIDDLEWARE = [
    ...
    "debug_toolbar.middleware.DebugToolbarMiddleware",
    ...
]

To:

MIDDLEWARE = [
    ...
    "strawberry_django_plus.middlewares.debug_toolbar.DebugToolbarMiddleware",
    ...
]

Contributing

We use poetry to manage dependencies, to get started follow these steps:

git clone https://github.com/blb-ventures/strawberry-django-plus
cd strawberry
poetry install
poetry run pytest

This will install all the dependencies (including dev ones) and run the tests.

Pre commit

We have a configuration for pre-commit, to add the hook run the following command:

pre-commit install

Licensing

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

Comments
  • The return type of the mutation

    The return type of the mutation

    Mutations return the Union type, and this makes returning scalars more difficult. I just wanted to know what was the reason for such a decision?

    https://github.com/blb-ventures/strawberry-django-plus/blob/e1578a2ae2393bf29c6d111180a1460cf7c1542a/strawberry_django_plus/mutations/fields.py#L130-L132

    question 
    opened by Luferov 11
  • Add fields on a relay connection?

    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()
    
    enhancement 
    opened by eloff 9
  • GlobalID vs ID scalar for Relay

    GlobalID vs ID scalar for Relay

    First off, thanks for a great library, it's pretty easy to use and the documentation helps a lot. My question is: what was the reasoning behind the decision to generate a GlobalID scalar type for Relay node types rather than using the ID type built into the GraphQL spec? I'm wondering because GlobalID seems to prevent the usage of directives in Relay such as @deleteEdge and @deleteRecord, because those expect to be applied to a response field of type ID when the response is actually giving GlobalID.

    For example, I have something like mutation:

    mutation AssetList_deleteAssetMutation(
      $input: NodeInput!
      $connections: [GlobalID!]!
    ) {
      deleteBeta(input: $input) {
        id @deleteEdge(connections: $connections) @deleteRecord
      }
    }
    

    and I get this error from the Relay compiler:

    Invalid use of @deleteEdge on field 'id'. Expected field type 'ID', got 'GlobalID!'
    

    I couldn't find any other references to this issue, so I'm wondering if I'm just doing something wrong or misunderstanding something? Thanks!

    opened by LucasPickering 8
  • Random

    Random "Event loop is closed" & "Future attached to a different loop" error on using dataloaders

    I have a section of code that does a consolidation based on database values. Currently, I'm using Django's Count, Max, Min to do so. However when queries involves these functions, the errors will be thrown randomly. I've tried figuring out the patterns but it seems that it just fails randomly.


    Environment attributes:

    • Python 3.10
    • strawberry-django-plus 1.21.0
    • strawberry-graphql 0.123.2
    • strawberry-graphql-django 0.4.0

    Here's my GQL request

    query ($vehicleTypeFilter: VehicleTypeFilter) {
        vehicleTypes (filters: $vehicleTypeFilter) {
            ...
            vehicleStatusNotSpottedCount
            vehicleTotalCount
            vehicleStatusInServiceCount
            vehicleStatusDecommissionedCount
            vehicleStatusTestingCount
        }
    }
    
    # Variables
    {
        "vehicleTypeFilter": {
            "lineId": 3
        }
    }
    

    Here are the logs (there are 2 types that occur quite randomly):

    1. Event loop is closed
    /root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/dataloader.py:146: RuntimeWarning: coroutine 'dispatch.<locals>.dispatch' was never awaited
      loader.loop.call_soon(create_task, dispatch())
    RuntimeWarning: Enable tracemalloc to get the object allocation traceback
    Event loop is closed
    
    GraphQL request:6:5
    5 |     displayName
    6 |     vehicleStatusDecommissionedCount
      |     ^
    7 |     vehicleStatusInServiceCount
    Traceback (most recent call last):
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/graphql/execution/execute.py", line 528, in await_result
        return_type, field_nodes, info, path, await result
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 500, in _async_resolver
        return await await_maybe(_get_result(_source, strawberry_info, **kwargs))
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/utils/await_maybe.py", line 12, in await_maybe
        return await value
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry_django_plus/utils/aio.py", line 83, in resolve_async
        ret = resolver(await value)
      File "/code/operation/schema/scalars.py", line 90, in vehicle_status_decommissioned_count
        return await vehicle_status_count_from_vehicle_type_loader.load(
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/dataloader.py", line 111, in load
        batch = get_current_batch(self)
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/dataloader.py", line 137, in get_current_batch
        dispatch(loader, loader.batch)
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/dataloader.py", line 146, in dispatch
        loader.loop.call_soon(create_task, dispatch())
      File "uvloop/loop.pyx", line 1279, in uvloop.loop.Loop.call_soon
      File "uvloop/loop.pyx", line 667, in uvloop.loop.Loop._call_soon
      File "uvloop/loop.pyx", line 676, in uvloop.loop.Loop._call_soon_handle
      File "uvloop/loop.pyx", line 671, in uvloop.loop.Loop._append_ready_handle
      File "uvloop/loop.pyx", line 703, in uvloop.loop.Loop._check_closed
    RuntimeError: Event loop is closed
    Stack (most recent call last):
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/django/views.py", line 257, in dispatch
        result = await self.schema.execute(
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/schema/schema.py", line 206, in execute
        self.process_errors(result.errors, execution_context=execution_context)
    
    1. got Future attached to a different loop
    Task <Task pending name='Task-594' coro=<ExecutionContext.execute_field.<locals>.await_result() running at /root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/graphql/execution/execute.py:528> cb=[gather.<locals>._done_callback() at /usr/local/lib/python3.10/asyncio/tasks.py:720]> got Future <Future pending> attached to a different loop
    
    GraphQL request:6:5
    5 |     displayName
    6 |     vehicleStatusDecommissionedCount
      |     ^
    7 |     vehicleStatusInServiceCount
    Traceback (most recent call last):
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/graphql/execution/execute.py", line 528, in await_result
        return_type, field_nodes, info, path, await result
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 500, in _async_resolver
        return await await_maybe(_get_result(_source, strawberry_info, **kwargs))
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/utils/await_maybe.py", line 12, in await_maybe
        return await value
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry_django_plus/utils/aio.py", line 83, in resolve_async
        ret = resolver(await value)
      File "/code/operation/schema/scalars.py", line 90, in vehicle_status_decommissioned_count
        return await vehicle_status_count_from_vehicle_type_loader.load(
    RuntimeError: Task <Task pending name='Task-594' coro=<ExecutionContext.execute_field.<locals>.await_result() running at /root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/graphql/execution/execute.py:528> cb=[gather.<locals>._done_callback() at /usr/local/lib/python3.10/asyncio/tasks.py:720]> got Future <Future pending> attached to a different loop
    Stack (most recent call last):
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/django/views.py", line 257, in dispatch
        result = await self.schema.execute(
      File "/root/.local/share/virtualenvs/code-_Py8Si6I/lib/python3.10/site-packages/strawberry/schema/schema.py", line 210, in execute
        self.process_errors(result.errors, execution_context=execution_context)
    

    Here is my relevant code

    operation/schema/loaders.py

    @sync_to_async
    def batch_load_vehicle_status_count_from_vehicle_type(keys):
        # Keys in format (vehicle_type_id, status)
        vehicle_object = Vehicle.objects.aggregate(
            **{
                str(key[0])
                + str(key[1]): Count("id", filter=Q(vehicle_type_id=key[0], status=key[1]))
                for key in keys
            }
        )
    
        return [vehicle_object.get(str(key[0]) + str(key[1]), None) for key in keys]
    
    vehicle_status_count_from_vehicle_type_loader = DataLoader(
        load_fn=batch_load_vehicle_status_count_from_vehicle_type
    )
    

    operation/schema/scalars.py

    @gql.django.type(models.VehicleType)
    class VehicleType:
        ...
        @gql.field
        async def vehicle_status_in_service_count(self) -> int:
            return await vehicle_status_count_from_vehicle_type_loader.load(
                (self.id, VehicleStatus.IN_SERVICE)
            )
    
        @gql.field
        async def vehicle_status_decommissioned_count(self) -> int:
            return await vehicle_status_count_from_vehicle_type_loader.load(
                (self.id, VehicleStatus.DECOMMISSIONED)
            )
    

    operation/schema/filters.py && schema.py

    @gql.type
    class OperationScalars:
        vehicleTypes: List[VehicleType] = gql.django.field(filters=VehicleTypeFilter)
    
    @gql.django.filters.filter(models.VehicleType)
    class VehicleTypeFilter:
        line_id: gql.ID
    
        def filter_line_id(self, queryset):
            return queryset.filter(vehicles__line_id=self.line_id).distinct()
    
    invalid question 
    opened by kwongtn 8
  • Input field Optional[OneToManyInput] is a required field in the resulting schema

    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.

    opened by gersmann 8
  • HasObjPerm directive on a update mutation fails updating the cache, due to unhashable type OperationInfo

    HasObjPerm directive on a update mutation fails updating the cache, due to unhashable type OperationInfo

    Specifying a HasObjPerm directive on a update mutation fails updating the cache, due to unhashable type OperationInfo. This is due to the resolver returning OperationInfo instead of FieldValueType, if the user does not have the expected permissions (in this casereporting.change_fieldvalue).

    The directive works for the 'good' case, where the permission is granted.

    I am wondering if I am missing something, or what the correct way of using object permissions on mutations would be. Should there be an exception on return value permission checking for OperationInfo?

    Mutation Setup:

    update_field_value: FieldValueType = gql.django.update_mutation(
            FieldValueInput,
            directives=[HasObjPerm("reporting.change_fieldvalue")],
        )
    

    Stack:

      File ".../app-QPiINJSB-py3.11/lib/python3.11/site-packages/strawberry_django_plus/utils/aio.py", line 156, in resolve
        ret = resolver(value)
              ^^^^^^^^^^^^^^^
      File ".../app-QPiINJSB-py3.11/lib/python3.11/site-packages/strawberry_django_plus/permissions.py", line 634, in resolve_for_user
        return self._resolve_obj_perms(helper, root, info, user, ret)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File ".../app-QPiINJSB-py3.11/lib/python3.11/site-packages/strawberry_django_plus/permissions.py", line 695, in _resolve_obj_perms
        has_perm = cache.get((self, obj))
                   ^^^^^^^^^^^^^^^^^^^^^^
    TypeError: unhashable type: 'OperationInfo'
    
    opened by gersmann 7
  • feat: enum type for standard django choices

    feat: enum type for standard django choices

    This PR enable support of enum for standard django choices. Basically it generate an Enum class on-the-fly with field choices as attributes.

    I need help/bootstrap to write a test on this.

    I'm able to specify the choice label on the schema documentation with

    enum_class_fields = {c[0]: strawberry.enum_value(c[0], description=c[1]) for c in field.choices}
    

    It work well on schema documentation but it crash using a query with this filter. Got the following error: "Expected value of type 'MyAutoEnum', found blabla; 'blabla' is not a valid MyAutoEnum" (Where MyAutoEnum is the Enum class and blabla a value inside it.)

    I made it to not have to use django-choices-field dependency just to support this. I thinks enum should work out-of-box with standard django choices.

    opened by fabien-michel 7
  • mutation with relations

    mutation with relations

    Using v1.20.2 This is my code:

    models.py

    class User(AbstractUser):
        // some fields
        ....
    
    class Profile(models.Model):
        user = models.OneToOneField(to=User, on_delete=models.CASCADE, primary_key=True)
        // some fields
        ...
    

    types.py

    @gql.django.type(models.User)
    class User(gql.Node):
        username: gql.auto
        avatar: gql.auto
        profile: 'Profile'
    
    
    @gql.django.partial(models.User)
    class UserInputPartial(gql.NodeInput):
        username: gql.auto
        avatar: gql.auto
        profile: Optional['ProfileInputPartial']
    
    
    @gql.django.type(models.Profile)
    class Profile(relay.Node):
        about: gql.auto
        gender: gql.auto
    
    
    @gql.django.partial(models.Profile)
    class ProfileInputPartial(gql.NodeInputPartial):
        about: gql.auto
        gender: gql.auto
    

    mutations.py which goes into schema

    @gql.type
    class UserMutations:
        update_user: types.User = gql.django.update_mutation(types.UserInputPartial)
    

    And when I run this Mutation:

    mutation MyMutation {
      updateUser(input: {id: "VXNlcjox", profile: {id: "UHJvZmlsZTox"}}) {
        ... on User {
          id
          username
          profile {
            id
          }
        }
      }
    }
    
    

    I get this kind of result

    {
      "data": {
        "updateUser": {
          "id": "VXNlcjox",
          "username": "mohammadmaso",
          "profile": {
            "id": "UHJvZmlsZTox"
          }
        }
      }
    }
    

    Why does profile have to specify id?

    question 
    opened by AriaMoradi 7
  • Need help on customize relay.Connection

    Need help on customize relay.Connection

    I have types like this and I want the CollectionNode to show photos with filter is_public=1 not all photos

    types:

    from strawberry_django_plus import gql
    from strawberry_django_plus.gql import relay
    
    @gql.django.type(Photo, filters=PhotoFilter, order=PhotoOrder)
    class PhotoNode(relay.Node):
        id: gql.auto
        title: gql.auto
    
        @classmethod
        def get_queryset(cls, queryset: QuerySet[Photo], _) -> QuerySet[Photo]:
            return queryset.filter(is_public=True)
    
    @gql.django.type(Collection, filters=CollectionFilter, order=CollectionOrder)
    class CollectionNode(relay.Node):
        id: gql.auto
        photos: relay.Connection["PhotoNode"] = relay.connection()
    

    schema :

    @gql.type
    class Query:
        """All available queries for this schema."""
    
        # Photos
        photo_list: relay.Connection[PhotoNode] = gql.django.connection(description='Return Photo connection with pagination information.')
    
        # Collection
        collection_list: relay.Connection[CollectionNode] = gql.django.connection(description='Return Collection connection with pagination information.')
    
    schema = gql.Schema(
        query=Query,
        extensions=[
            SchemaDirectiveExtension,
            DjangoOptimizerExtension,
        ],
    )
    
    

    photo_list query is fine and shows public photos but collection_list -> photos not working and show all photos

    opened by sahmadreza 6
  • resolve_nodes not working when using gql.django.connection ?

    resolve_nodes not working when using gql.django.connection ?

    When I try to resolve_nodes to make a custom query, it doesn't affect the result, and when I add the print statement on resolve_nodes function, it does not appear on the console.

    opened by darwinhace 6
  • CUD mutations Input vs NodeInput

    CUD mutations Input vs NodeInput

    Hi @bellini666, quick question about the CUD mutations. They are expecting a NodeInput as input, but from the implementation I can see that both, ID and GlobalID cases are managed, when I tested this it worked fine with ID.

    Would it make sense to update/relax the input type annotation?

    question 
    opened by gersmann 5
  • Non-obvious moments when overloading the Connection

    Non-obvious moments when overloading the Connection

    Hello I tried to define a custom Connection class to extend it with the data I need. For this, the Node class was also redefined, and I encountered non-obvious behavior when defining the resolve_connection method

    def _has_own_node_resolver(cls, name: str) -> bool:
        resolver = getattr(cls, name, None)
        if resolver is None:
            return False
    
        if id(resolver.__func__) == id(getattr(Node, name).__func__):
            return False
    
        return True
    

    It turns out that the method described in the Node class is replaced by default with another one, and when defining a custom class, it is impossible to call super of the parent class In other words, when defining a class and at runtime, we have different resolvers What is the reason for this behaviour? Wouldn't it be better to immediately set the necessary behavior in the Node class?

    enhancement help wanted 
    opened by iamcrookedman 1
  • Optimizer causes duplicate query when filtering or paginating a related field

    Optimizer causes duplicate query when filtering or paginating a related field

    Given a schema like:

    class Foo(Model):
        pass
    
    class Bar(Model):
        status = CharField()
        foo = ForeignKey(Foo, related_name="bars")
    
    @gql.django.type(Foo)
    class FooType:
        id: auto
        bars: List["BarType"]
    
    @gql.django.filters.filter(Bar)
    class BarFilter:
        status: str
    
    @gql.django.type(Bar, filters=BarFilter, pagination=True)
    class BarType:
        id: auto
    

    and a query like:

    {
      foo(id: 1) {
        bars(filters: {status: "whatever"}) {
          id
        }
      }
    }
    

    the optimizer will cause two DB queries against the bar table. The first is because the optimizer determines that we need to prefetch bars, and it does so without the filters. A second query is then done with the WHERE clause for the filters.

    Aside from being inefficient, this prefetching also causes an error when paginating the related field. Given the following query:

    {
      foo(id: 1) {
        bars(pagination: {limit: 2}) {
          id
        }
      }
    }
    

    The optimizer will perform the prefetch without any pagination. A second query is then done by slicing the queryset here: https://github.com/strawberry-graphql/strawberry-graphql-django/blob/d57d767dc9574030888b5c36db4869e54bd24aff/strawberry_django/pagination.py#L25

    The problem is that because the queryset (.all() on the related manager) has already been evaluated by the optimizer's prefetch, slicing it for pagination returns a list rather than another queryset. The list then propagates down to get_queryset_as_list here: https://github.com/blb-ventures/strawberry-django-plus/blob/a6359564da28b757dcac3b9dae08f22cb2fba336/strawberry_django_plus/field.py#L261-L266

    and because _fetch_all doesn't exist on a list, it raises an exception.


    I think these might be two separate issues but I discovered the first while investigating the second. Let me know if it would be better to split these into separate github issues.

    I'd imagine the pagination issue could be solved with an isinstance(qs, QuerySet) check before calling _fetch_all, but preferably the optimizer would determine if we need to do any filtering/pagination/ordering and do that in the prefetch the first time so that a second query isn't performed at all.

    bug help wanted 
    opened by vitosamson 5
  • Cursor Pagination without relay

    Cursor Pagination without relay

    Hello and merry xmas.

    I just finished an implementation of cursor pagination for a non-relay context and I encountered a couple of issues I would like to ask about and perhaps get a few pointers on a better implementation. Pagination was implemented using a modified version of django-cursor-pagination , sub-classing StrawberryDjangoField , implementing a generic type for the pagination result and a custom field function to return and pass options to the instance of the sub classed StrawberryDjangoField

    Field Implementation:

    @strawberry.type
    class PageInfo:
        has_next_page: bool
        has_previous_page: bool
        start_cursor: Optional[str]
        end_cursor: Optional[str]
    
    E = TypeVar("E")
    @strawberry.type
    class CursorPaginatedList(Generic[E]):
        page_info: PageInfo
        items: List[E]
    
    @strawberry.input
    class CursorPaginationInput:
        first: Optional[int] = None
        last: Optional[int] = None
        before: Optional[str] = None
        after: Optional[str] = None
    
    class CursoredGenericDjangoField(
        _StrawberryDjangoField
    ):
        def __init__(self, cursor_ordering=UNSET,  **kwargs):
            self.cursor_pagination = kwargs.pop('cursor_pagination',False)
            #remove other pagination and order if defined
            if self.cursor_pagination:
                kwargs.pop('order')
                kwargs.pop('pagination')
            super().__init__(**kwargs)
    
        @property
        def arguments(self) -> List[StrawberryArgument]:
            arguments = []
            if self.cursor_pagination:
                arguments.append(argument("cursor_pagination",CursorPaginationInput))
                arguments.append(argument("cursor_ordering",Optional[str]))
            return super().arguments + arguments
    
        @property
        def is_list(self):
            if self.cursor_pagination is UNSET or not self.cursor_pagination:
                return super().is_list
            return True
    
        @property
        def type(self) -> Union[StrawberryType, type]:
            return super().type
    
        @type.setter
        def type(self, type_: Any) -> None:
            super(CursoredGenericDjangoField, self.__class__).type.fset(self, type_)
            #store type and inner type to use in return of result and get_queryset to grab 
            #any get_queryset overload that exists on inner type definition
            if type_ is not None and self.cursor_pagination:
                self.paginated_type = type_
                self.inner_type = typing.get_args(type_)[0]
    
    
        #resolvers.resolve_result returns a coroutine which needs to be awaited so we can get a list and slice it
        #so this needs to be async. Could not figure out a better way to do this.
        async def rewrap_result(self,result,**kwargs):
            qls = list(await result)
            cp = kwargs.get('cursor_pagination',None)
            if cp and len(qls):
                page = self.paginator.page_from_list(qls,first=cp.first , last=cp.last , after=cp.after, before=cp.before)
                pi = PageInfo(
                    has_next_page= page.has_next,
                    has_previous_page= page.has_previous,
                    start_cursor=page.paginator.cursor(page.items[0]),
                    end_cursor=page.paginator.cursor(page.items[-1]),
                )
                res =  self.paginated_type(
                    page_info=pi,
                    items=page.items,
                )
            else:
                pi = PageInfo(
                    has_next_page= False,
                    has_previous_page= False,
                    start_cursor=None,
                    end_cursor=None,
                )
                res =  self.paginated_type(
                    page_info=pi,
                    items=qls,
                )
            return res
    
        def get_result(
            self,
            source: Optional[models.Model],
            info: Info,
            args: List[Any],
            kwargs: Dict[str, Any],
        ) -> Union[Awaitable[Any], Any]:
            if self.cursor_pagination:
                return self.rewrap_result(super().get_result(source , info , args , kwargs), **kwargs)
            return super().get_result(source , info , args , kwargs)        
    
        def paginate(self , qs , **kwargs):
            cp = kwargs.get('cursor_pagination')
            order = kwargs.get('cursor_ordering','id')
            self.paginator = CursorPaginator(qs, ordering=(order,'-id'))
            #Had to modify the paginator to split it's original page method into
            #two separate methods one adding filters to the queryset and another to slice the result
            #because resolvers.resolve_result returns a coroutine and not a queryset
            page = self.paginator.page_queryset(first=cp.first, after=cp.after , last=cp.last , before=cp.before)
            return page
    
        def get_queryset(self, queryset, info, order=UNSET, **kwargs):
            if self.cursor_pagination:
                inner_get_queryset = getattr(self.inner_type,"get_queryset",None)
                #Apply inner get queryset defined on the paginated type
                if inner_get_queryset:
                    queryset = inner_get_queryset(queryset, info, **kwargs)
                #Get the queryset from super now so we get filtering on it
                queryset =  super().get_queryset(queryset, info, order=order, **kwargs)
                #Apply pagination and ordering or just ordering based on one 
                #unique field as required for cursor pagination
                if kwargs.get('cursor_pagination',None):
                    queryset = self.paginate(queryset,**kwargs)
                else:
                    ob = kwargs.get('cursor_ordering',None)
                    if ob:
                        queryset = queryset.order_by(ob)
                return queryset
            return super().get_queryset(queryset, info, order=order, **kwargs)
    
    #re-defining the field function would not be necessary if:
    #  1) Any number of extra params would be passed to the constructor of the field class
    #  2) a field_cls parameter would allow for selection of which class would be returned like with type
    def field(
        resolver=None,
        *,
        name: Optional[str] = None,
        field_name: Optional[str] = None,
        is_subscription: bool = False,
        description: Optional[str] = None,
        permission_classes: Optional[List[Type[BasePermission]]] = None,
        deprecation_reason: Optional[str] = None,
        default: Any = dataclasses.MISSING,
        default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
        metadata: Optional[Mapping[Any, Any]] = None,
        directives: Optional[Sequence[object]] = (),
        pagination: Optional[bool] = UNSET,
        filters: Optional[type] = UNSET,
        order: Optional[type] = UNSET,
        only: Optional[TypeOrSequence[str]] = None,
        select_related: Optional[TypeOrSequence[str]] = None,
        prefetch_related: Optional[TypeOrSequence[PrefetchType]] = None,
        disable_optimization: bool = False,
        init: Literal[True, False, None] = None,
        #This was added to allow cursor_pagination=True to be passed to the instance
        #or any other parameter that we may require in other subclasses of StrawberryDjangoField
        **kwargs
    ) -> Any:
    
        f = CursoredGenericDjangoField(
           python_name=None,
           django_name=field_name,
           graphql_name=name,
           type_annotation=None,
           description=description,
           is_subscription=is_subscription,
           permission_classes=permission_classes or [],
           deprecation_reason=deprecation_reason,
           default=default,
           default_factory=default_factory,
           metadata=metadata,
           directives=directives,
           filters=filters,
           pagination=pagination,
           order=order,
           only=only,
           select_related=select_related,
           prefetch_related=prefetch_related,
           disable_optimization=disable_optimization,
           **kwargs
        )
        if resolver:
            f = f(resolver)
        return f
    
    

    Usage:

    @strawberry.type
    class Query:
        paginated_list:  module.CursorPaginatedList[SomeType] = module.field(cursor_pagination=True , filters=SomeFilter)
    

    Or CursorPaginatedList could be sub classed to add more fields to it.

    Issues:

    1. Is there any way to make the field function in this package more "generic" so it can instantiate any subclass of StrawberryDjangoField passing to it any number of extra arguments and leave it to the user to pop them so they don't get passed down the inheritance chain ?
    2. resolvers.resolve_result returns a coroutine which forced me to use the workarounds outlined in the comments and to modify the paginators functionality splitting the filtering on the queryset from the slicing of the result. Can you suggest any workaround on this?
    3. My initial instinct was to detect whether the type is a subclass of CursorPaginatedList and not have to pass a cursor_pagination=True to the constructor but I could not find a way to detect the type within the init to disable default order and pagination. Also couldn't find a reliable way to detect whether a type is a subclass for that matter. Any suggestions ?

    Enhancements:

    1. While reading through the code to get hints for my implementation I noticed that it's very tightly coupled with the relay implementation which made it kind of hard to figure out which part needed to be re-implemented. Perhaps the relay implementation should be completely separate ?
    2. StrawberryDjangoField is a subclass of strawberry_django.field.StrawberryDjangoField which has multiple inheritance on the pagination , filter and ordering classes. It would be nice if a bare minimally working version of StrawberryDjangoField was provided so a user can do his own composition on the field choosing to use , not use or maybe replace some of these classes.

    Thanks in advance for any comments or suggestions you may have on this.

    question 
    opened by m4riok 2
  • Query optimization based on type hints for non-model fields?

    Query optimization based on type hints for non-model fields?

    A field that represents a model property will not be handled by the query optimizer: https://github.com/blb-ventures/strawberry-django-plus/blob/9a54dd0378b31302b99754704ccd9f1025f43fbd/strawberry_django_plus/optimizer.py#L163-L166

    It could get derived from the type hint though, couldn't it?

    Specifying field_name, which gets translated into django_name works. Is this supposed to work like/for this? Are there unwanted side effects given that it is of type Foo, but not necessarily the actual foreign key / linked object?

        special_foo: Foo = gql.django.field(
            field_name="foo",  # workaround? other side effects?
        )
    

    As for now I am happy with adding only/select_related manually (related: https://github.com/blb-ventures/strawberry-django-plus/issues/152).

    enhancement help wanted 
    opened by blueyed 2
  • FieldError when using select_related with enable_only_optimization

    FieldError when using select_related with enable_only_optimization

    When trying to use select_related on a field, I am getting an error when the DjangoOptimizerExtension is used with its default enable_only_optimization=True.

    This happens because only() is used with "id", and then select_related is issued on top: https://github.com/blb-ventures/strawberry-django-plus/blob/9a54dd0378b31302b99754704ccd9f1025f43fbd/strawberry_django_plus/optimizer.py#L529-L533

    django.core.exceptions.FieldError: Field Foo.bar cannot be both deferred and traversed using select_related at the same time.

    A workaround is to add the field also to only, but that could be done automatically then also.

    @django_type(models.Foo, filters=FooFilter, pagination=True)
    class Foo:
        bar: Bar
        special_bar: Bar | None = gql.django.field(
            only=["bar"],  # workaround
            select_related=["bar"]
        )
    

    I am not sure about the select_related being necessary/useful here yet (maybe because of the Optional/None it might be necessary), but I think it's a generic issue.

    I also wondered what's the point of enable_only_optimization. Is it to reduce the amount of data being selected/transferred only?

    (related)

    enhancement 
    opened by blueyed 4
  • DjangoOptimizerExtension not optimizing when fragments are used and field is queried twice

    DjangoOptimizerExtension not optimizing when fragments are used and field is queried twice

    Given this schema (simplified from the demo):

    schema.py
    from typing import List, cast
    
    from strawberry_django_plus import gql
    from strawberry_django_plus.directives import SchemaDirectiveExtension
    from strawberry_django_plus.optimizer import DjangoOptimizerExtension
    
    from .models import Issue, Tag
    
    
    @gql.django.type(Issue)
    class IssueType:
        name: gql.auto
        tags: List["TagType"]
    
    
    @gql.django.type(Tag)
    class TagType:
        name: gql.auto
        issues: List[IssueType]
    
    
    @gql.type
    class Query:
        issue_list: List[IssueType] = gql.django.field()
    
    
    schema = gql.Schema(
        query=Query,
        extensions=[
            SchemaDirectiveExtension,
            DjangoOptimizerExtension,
        ],
    )
    

    And given these two equivalent queries:

    slow.graphql
    query Slow {
      issueList {
        name
        tags {
          ...A
        }
        tags {
          name
        }
      }
    }
    
    fragment A on TagType {
      issues {
        ...B
      }
    }
    
    fragment B on IssueType {
      name
    }
    
    fast.graphql (obtained by clicking on "Merge fragments into query" in GraphiQL)
    query Fast {
      issueList {
        name
        tags {
          issues {
            name
          }
          name
        }
      }
    }
    

    Initializing the database with this queries:

    Details
    INSERT INTO demo_issue (id, name, priority)
    VALUES (1, 'issue1', 1), (2, 'issue2', 2), (3, 'issue3', 3);
    
    INSERT INTO demo_tag (id, name)
    VALUES (1, 'tag1'), (2, 'tag2'), (3, 'tag3');
    
    INSERT INTO demo_issue_tags (issue_id, tag_id)
    VALUES (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3);
    

    Slow is experiencing the N+1 queries problem, while Fast is not.

    Using GraphiQL and the Django toolbar I can see that Fast is making 3 queries:

    image

    While Slow is making 11:

    image

    Note that moving the field name from issueList into the fragment A doesn't trigger the issue.

    bug 
    opened by edomora97 2
Releases(v1.34)
  • v1.34(Jan 4, 2023)

    What's Changed

    • feat: Support full_clean when directly invoking resolver by @parrotmac in https://github.com/blb-ventures/strawberry-django-plus/pull/149

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.33.2...v1.34

    Source code(tar.gz)
    Source code(zip)
  • v1.33.2(Jan 4, 2023)

    What's Changed

    • doc: fix typo on optimizer.py by @nrbnlulu in https://github.com/blb-ventures/strawberry-django-plus/pull/161
    • fix: Cast object_pk to model pk type by @parrotmac in https://github.com/blb-ventures/strawberry-django-plus/pull/148

    New Contributors

    • @parrotmac made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/148

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.33.1...v1.33.2

    Source code(tar.gz)
    Source code(zip)
  • v1.33.1(Dec 28, 2022)

    What's changed

    • fix: Do not check permissions for OperationInfo

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.33...v1.33.1

    Source code(tar.gz)
    Source code(zip)
  • v1.33(Dec 27, 2022)

    What's changed

    • feat: Make defined types comparable and hashable

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.32.4...v1.33

    Source code(tar.gz)
    Source code(zip)
  • v1.32.4(Dec 27, 2022)

    What's changed

    • fix: Remove print statement left behind

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.32.3...v1.32.4

    Source code(tar.gz)
    Source code(zip)
  • v1.32.3(Dec 26, 2022)

    What's changed

    • fix: Only should only be aborted when the whole model is specified (i.e. as a string) (#156)

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.32.2...v1.32.3

    Source code(tar.gz)
    Source code(zip)
  • v1.32.2(Dec 22, 2022)

    What's Changed

    • fix: allow update of nested input fields on foreign key relationships by @gersmann in https://github.com/blb-ventures/strawberry-django-plus/pull/155

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.32.1...v1.32.2

    Source code(tar.gz)
    Source code(zip)
  • v1.32.1(Dec 19, 2022)

    What's Changed

    • fix: store auto enum in model field hidden attr to avoid duplication in schema by @fabien-michel in https://github.com/blb-ventures/strawberry-django-plus/pull/151

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.32...v1.32.1

    Source code(tar.gz)
    Source code(zip)
  • v1.32(Dec 5, 2022)

    What's Changed

    • fix: deletes instead of removes non-nullable one to many related objects on nested update by @gersmann in https://github.com/blb-ventures/strawberry-django-plus/pull/147

    New Contributors

    • @gersmann made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/147

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.31.1...v1.32

    Source code(tar.gz)
    Source code(zip)
  • v1.31.1(Nov 26, 2022)

    What's changed

    • fix: Fix strange mypy issue introduced in https://github.com/blb-ventures/strawberry-django-plus/pull/137

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.31...v1.31.1

    Source code(tar.gz)
    Source code(zip)
  • v1.31(Nov 23, 2022)

    What's Changed

    • feat: enum type for standard django choices by @fabien-michel in https://github.com/blb-ventures/strawberry-django-plus/pull/137

    New Contributors

    • @fabien-michel made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/137

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/1.30.1...v1.31

    Source code(tar.gz)
    Source code(zip)
  • 1.30.1(Nov 21, 2022)

    What's changed

    • fix: Workaround adding order/filters in django connections in a thread-safe way

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.30...1.30.1

    Source code(tar.gz)
    Source code(zip)
  • v1.30(Nov 15, 2022)

    What's Changed

    • feat: allow mutation full clean with kwargs by @OdysseyJ in https://github.com/blb-ventures/strawberry-django-plus/pull/140

    New Contributors

    • @OdysseyJ made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/140

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.29...v1.30

    Source code(tar.gz)
    Source code(zip)
  • v1.29(Nov 10, 2022)

    What's Changed

    • feat: Support strawberry 0.139+
    • Update debug-toolbar.md in https://github.com/blb-ventures/strawberry-django-plus/pull/141

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.28.6...v1.29

    Source code(tar.gz)
    Source code(zip)
  • v1.28.6(Oct 28, 2022)

  • v1.28.5(Oct 19, 2022)

    What's changed

    • fix: Keep django_name when it is provided by the user

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.28.4...v1.28.5

    Source code(tar.gz)
    Source code(zip)
  • v1.28.4(Oct 17, 2022)

    What's Changed

    • [#134] Add is_basic_field to RelayField by @LucasPickering in https://github.com/blb-ventures/strawberry-django-plus/pull/135

    New Contributors

    • @LucasPickering made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/135

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.28.3...v1.28.4

    Source code(tar.gz)
    Source code(zip)
  • v1.28.3(Oct 11, 2022)

    What's Changed

    • fix: Strawberry 0.133.1+ expects the default factory to be MISSING instead of a lambda that returns UNSET by @bellini666 in https://github.com/blb-ventures/strawberry-django-plus/pull/132

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.28.2...v1.28.3

    Source code(tar.gz)
    Source code(zip)
  • v1.28.2(Oct 4, 2022)

    What's Changed

    • Add py.typed file by @patrick91 in https://github.com/blb-ventures/strawberry-django-plus/pull/127

    New Contributors

    • @patrick91 made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/127

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.28.1...v1.28.2

    Source code(tar.gz)
    Source code(zip)
  • v1.28.1(Oct 3, 2022)

    What's Changed

    • Add return type hint for ConditionDirective.check_condition by @edomora97 in https://github.com/blb-ventures/strawberry-django-plus/pull/125

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.28...v1.28.1

    Source code(tar.gz)
    Source code(zip)
  • v1.28(Oct 1, 2022)

    What's changed

    • fix: Fix resolving issues with latest changes from strawberry-graphql

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.27...v1.28

    Source code(tar.gz)
    Source code(zip)
  • v1.27(Sep 23, 2022)

    What's changed

    • feat: Support field metadata from upstream strawberry
    • fix: Force is_basic_field to return False to fix resolving issues with our custom fields (to support strawberry >= 0.132.1)

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.26.1...v1.27

    Source code(tar.gz)
    Source code(zip)
  • v1.26.1(Sep 12, 2022)

    What's changed

    • Fix order parameter not being added to Connection after latest changes from strawberry-graphql-django
    • Fix relay with lazy types after latest changes from upstream strawberry

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.26...v1.26.1

    Source code(tar.gz)
    Source code(zip)
  • v1.26(Sep 12, 2022)

    What's changed

    • Add support for strawberry-graphql-django >= 0.5.0

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.25.2...v1.26

    Source code(tar.gz)
    Source code(zip)
  • v1.25.2(Sep 5, 2022)

    What's Changed

    • Make Debug Toolbar work on GraphiQl 2 by @Kitefiko in https://github.com/blb-ventures/strawberry-django-plus/pull/112

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.25.1...v1.25.2

    Source code(tar.gz)
    Source code(zip)
  • v1.25.1(Sep 1, 2022)

    What's changed

    • feat: Use the CONNECTION_CLASS defined on node when creating a connection field

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.25...v1.25.1

    Source code(tar.gz)
    Source code(zip)
  • v1.25(Aug 29, 2022)

    What's Changed

    • Update docs for custom mutations by @oleo65 in https://github.com/blb-ventures/strawberry-django-plus/pull/105
    • Feat: Customizable typename; Fix: GlobalID parse_literal, UNSET as default_value by @Kitefiko in https://github.com/blb-ventures/strawberry-django-plus/pull/106

    New Contributors

    • @oleo65 made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/105

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.24.1...v1.25

    Source code(tar.gz)
    Source code(zip)
  • v1.24.1(Aug 23, 2022)

    What's Changed

    • fix: Abort only optimization when manually prefetching something
    • fix: broken django-guardian link by @zacatac in https://github.com/blb-ventures/strawberry-django-plus/pull/102

    New Contributors

    • @zacatac made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/102

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.24...v1.24.1

    Source code(tar.gz)
    Source code(zip)
  • v1.24(Aug 23, 2022)

    What's Changed

    • feat: Add support to optimize GenericRelation fields
    • feat: Support only/select_related hints for many relations
    • feat: Optimize GenericForeignKey by prefetch_related them
    • Fix pre-commit configuration by @edomora97 in https://github.com/blb-ventures/strawberry-django-plus/pull/101

    New Contributors

    • @edomora97 made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/101

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.23...v1.24

    Source code(tar.gz)
    Source code(zip)
  • v1.23(Aug 12, 2022)

    What's Changed

    • feat: Allow to change the Connection class of a Node and also the Edge class of a Connection for custom relay implementations
    • docs: fix link to django-guardian docs by @PabloAlexis611 in https://github.com/blb-ventures/strawberry-django-plus/pull/97
    • fix(optimizer): avoid double add_prefix calls by @wodCZ in https://github.com/blb-ventures/strawberry-django-plus/pull/99

    New Contributors

    • @PabloAlexis611 made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/97
    • @wodCZ made their first contribution in https://github.com/blb-ventures/strawberry-django-plus/pull/99

    Full Changelog: https://github.com/blb-ventures/strawberry-django-plus/compare/v1.22...v1.23

    Source code(tar.gz)
    Source code(zip)
Owner
BLB Ventures
Venture Builder
BLB Ventures
A Django GraphQL (Graphene) base template

backend A Django GraphQL (Graphene) base template Make sure your IDE/Editor has Black and EditorConfig plugins installed; and configure it lint file a

Reckonsys 4 May 25, 2022
Django application and library for importing and exporting data with admin integration.

django-import-export django-import-export is a Django application and library for importing and exporting data with included admin integration. Featur

null 2.6k Dec 26, 2022
Django admin CKEditor integration.

Django CKEditor NOTICE: django-ckeditor 5 has backward incompatible code moves against 4.5.1. File upload support has been moved to ckeditor_uploader.

null 2.2k Jan 2, 2023
A simple app that provides django integration for RQ (Redis Queue)

Django-RQ Django integration with RQ, a Redis based Python queuing library. Django-RQ is a simple app that allows you to configure your queues in djan

RQ 1.6k Jan 6, 2023
Bootstrap 3 integration with Django.

django-bootstrap3 Bootstrap 3 integration for Django. Goal The goal of this project is to seamlessly blend Django and Bootstrap 3. Want to use Bootstr

Zostera B.V. 2.3k Jan 3, 2023
Bootstrap 4 integration with Django.

django-bootstrap 4 Bootstrap 4 integration for Django. Goal The goal of this project is to seamlessly blend Django and Bootstrap 4. Requirements Pytho

Zostera B.V. 980 Dec 29, 2022
Plug and play continuous integration with django and jenkins

django-jenkins Plug and play continuous integration with Django and Jenkins Installation From PyPI: $ pip install django-jenkins Or by downloading th

Mikhail Podgurskiy 941 Oct 22, 2022
A django integration for huey task queue that supports multi queue management

django-huey This package is an extension of huey contrib djhuey package that allows users to manage multiple queues. Installation Using pip package ma

GAIA Software 32 Nov 26, 2022
Django admin CKEditor integration.

Django CKEditor NOTICE: django-ckeditor 5 has backward incompatible code moves against 4.5.1. File upload support has been moved to ckeditor_uploader.

null 2.2k Dec 31, 2022
TinyMCE integration for Django

django-tinymce django-tinymce is a Django application that contains a widget to render a form field as a TinyMCE editor. Quickstart Install django-tin

Jazzband 1.1k Dec 26, 2022
Bootstrap 3 integration with Django.

django-bootstrap3 Bootstrap 3 integration for Django. Goal The goal of this project is to seamlessly blend Django and Bootstrap 3. Want to use Bootstr

Zostera B.V. 2.3k Jan 2, 2023
Django + Next.js integration

Django Next.js Django + Next.js integration From a comment on StackOverflow: Run 2 ports on the same server. One for django (public facing) and one fo

Quera 162 Jan 3, 2023
Django URL Shortener is a Django app to to include URL Shortening feature in your Django Project

Django URL Shortener Django URL Shortener is a Django app to to include URL Shortening feature in your Django Project Install this package to your Dja

Rishav Sinha 4 Nov 18, 2021
Meta package to combine turbo-django and stimulus-django

Hotwire + Django This repository aims to help you integrate Hotwire with Django ?? Inspiration might be taken from @hotwired/hotwire-rails. We are sti

Hotwire for Django 31 Aug 9, 2022
django-reversion is an extension to the Django web framework that provides version control for model instances.

django-reversion django-reversion is an extension to the Django web framework that provides version control for model instances. Requirements Python 3

Dave Hall 2.8k Jan 2, 2023
Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application.

Django-environ django-environ allows you to use Twelve-factor methodology to configure your Django application with environment variables. import envi

Daniele Faraglia 2.7k Jan 7, 2023
Rosetta is a Django application that eases the translation process of your Django projects

Rosetta Rosetta is a Django application that facilitates the translation process of your Django projects. Because it doesn't export any models, Rosett

Marco Bonetti 909 Dec 26, 2022
Cookiecutter Django is a framework for jumpstarting production-ready Django projects quickly.

Cookiecutter Django Powered by Cookiecutter, Cookiecutter Django is a framework for jumpstarting production-ready Django projects quickly. Documentati

Daniel Feldroy 10k Dec 31, 2022
django-quill-editor makes Quill.js easy to use on Django Forms and admin sites

django-quill-editor django-quill-editor makes Quill.js easy to use on Django Forms and admin sites No configuration required for static files! The ent

lhy 139 Dec 5, 2022