strawberry-graphql / strawberry-django

Strawberry GraphQL Django extension
MIT License
391 stars 115 forks source link

datetime filters don't work with custom resolver lookups #445

Closed mayteio closed 3 weeks ago

mayteio commented 6 months ago

Describe the Bug

I'm trying to set up a simple paginated model that can filter based on the created_at field for a model like:

class Message(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    owner = models.CharField(max_length=32, db_index=True)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

Here's my custom resolver that filters messages based on the owner field and the authenticated user:

from datetime import datetime

import strawberry
import strawberry_django
from strawberry_django.filters import FilterLookup
from strawberry import auto

from chat import models
from .context import APIInfo

@strawberry_django.filter(models.Message, lookups=True)
class MessageFilter:
    created_at: datetime

@strawberry_django.order(models.Message)
class MessageOrder:
    created_at: auto

@strawberry_django.type(
    models.Message,
    order=MessageOrder,
    filters=MessageFilter,
    pagination=True,
)
class Message:
    id: auto
    content: auto
    created_at: datetime

class NotAuthenticatedError(Exception):
    code = "UNAUTHENTICATED_ERROR"
    message = "User not authenticated"

@strawberry.type
class Query:
    @strawberry_django.field
    def messages(
        self,
        info: APIInfo,
        filters: MessageFilter | None = strawberry.UNSET,
        order: MessageOrder | None = strawberry.UNSET,
    ) -> list[Message]:
        if not info.context.user:
            raise NotAuthenticatedError()

        qs = models.Message.objects.all()

        # filter to active firebase user
        qs.filter(owner=info.context.user.uid)

        if filters is not strawberry.UNSET:
            qs = strawberry_django.filters.apply(filters, qs, info)

        if order is not strawberry.UNSET:
            qs = strawberry_django.ordering.apply(filters, qs)

        return qs

However, when I run the following query:

query {
  messages(order:{createdAt:DESC}, filters:{createdAt:{gt:"2023-12-21T22:26:06.691087"}}) {
    createdAt
    content
  }
}

I get the following error:

{
  "data": null,
  "errors": [
    {
      "message": "Value cannot represent a DateTime: \"{'gt': '2023-12-21T22:26:06.691087'}\". ISO string too short"
    }
  ]
}

It appears to me it's trying to parse the entire lookup filter as a datetime.

So if I instead annotate the filter directly using the FilterLookup type:

@strawberry_django.filter(models.Message)
class MessageFilter:
    created_at: FilterLookup[datetime]

I get the following error when running the same query:

{
  "data": null,
  "errors": [
    {
      "message": "'datetime.datetime' object has no attribute '__strawberry_definition__'",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "messages"
      ]
    }
  ]
}

I've searched the issues and docs and there are no examples of filtering by datetime. I guess I could roll this myself but I was wondering if it's possible to do within the scope of the strawberry-graphql-django 🙂

What am I doing wrong here?

Upvote & Fund

Fund with Polar

bellini666 commented 6 months ago

Mey @mayteio ,

Hrm, that's strange. Both this:

@strawberry_django.filter(models.Message, lookups=True)
class MessageFilter:
    created_at: auto

Or this:

@strawberry_django.filter(models.Message)
class MessageFilter:
    created_at: FilterLookup[datetime]

Should work. Both are also equivalent as auto would translate to datetime and lookups=True would make it FilterLookup[datetime]

Do you have the full traceback of that? I'll try to investigate that later

bellini666 commented 5 months ago

@mayteio AHHH, taking a look at this again know I see what is going on... Note the message "Value cannot represent a DateTime: \"{'gt': '2023-12-21T22:26:06.691087'}\". ISO string too short"

This message is from dateutil.parser.isoparse trying to parse the string "{'gt': '2023-12-21T22:26:06.691087'}" instead of'2023-12-21T22:26:06.691087'`

That is happening because you defined the filter like:

@strawberry_django.filter(models.Message, lookups=True)
class MessageFilter:
    created_at: datetime

The lookups=True only have effect on auto fields, what you type explicitally will be respected.

If you want the lookups you can either do created_at: bool or, if you want to use datetime directly:

from strawberry_django.filters import FilterLookup

@strawberry_django.filter(models.Message, lookups=True)
class MessageFilter:
    created_at: FilterLookup[datetime]

Let me know if that solves your issue

mayteio commented 5 months ago

Thanks for the insight @bellini666! I will loop back to this soon 😄

bellini666 commented 3 weeks ago

Hey @mayteio ,

I'm considering that this has been solved for you? If not let me know so that we can reopen this