Răsfoiți Sursa

chore(deps): Upgrade strawberry-graphql-django to v0.93.0

Upgrades strawberry-graphql-django to v0.93.0 and strawberry-graphql to
v0.334.2, which must be upgraded together.

strawberry-graphql-django v0.93.0 expects a list field's queryset to
remain a QuerySet after pagination, but slicing a queryset which has
already been evaluated by a prefetch_related hint returns a list. Update
apply_pagination() to slice the cached results of an evaluated queryset
and return them on a clone instead, preserving both explicit pagination
and MAX_PAGE_SIZE enforcement on prefetched list fields (e.g. tags).

Closes #23383

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jeremy Stretch 4 zile în urmă
părinte
comite
4beb3a779f

+ 13 - 0
netbox/netbox/graphql/pagination.py

@@ -136,4 +136,17 @@ def apply_pagination(
             limit=pagination.limit,
         )
 
+    # Slicing a queryset which has already been evaluated (e.g. by a `prefetch_related` hint on the field)
+    # returns a list rather than a QuerySet, which strawberry-django's list hook cannot handle. Instead, slice
+    # the cached results and return them on a clone of the queryset.
+    if pagination is not None and related_field_id is None and queryset._result_cache is not None:
+        start = pagination.offset
+        limit = _resolve_limit(pagination.limit)
+        stop = start + limit if limit is not None and limit >= 0 else None
+        results = queryset._result_cache[start:stop]
+        queryset = queryset._chain()
+        queryset._result_cache = results
+        queryset._prefetch_done = True
+        return queryset
+
     return apply(pagination, queryset, related_field_id=related_field_id)

+ 60 - 0
netbox/netbox/tests/test_graphql.py

@@ -613,6 +613,66 @@ class GraphQLAPITestCase(APITestCase):
             msg=f'Expected batched tag prefetch, got {tag_queries} tag queries for 10 devices',
         )
 
+    def test_graphql_device_list_tags_pagination(self):
+        """
+        Pagination can be applied to a prefetched list field (tags).
+        """
+        self.add_permissions('dcim.view_device', 'extras.view_tag')
+
+        manufacturer = Manufacturer.objects.create(name='Pagination Manufacturer', slug='pagination-manufacturer')
+        device_type = DeviceType.objects.create(
+            manufacturer=manufacturer,
+            model='Pagination Model',
+            slug='pagination-model',
+        )
+        device_role = DeviceRole.objects.create(name='Pagination Role', slug='pagination-role')
+        device = Device.objects.create(
+            name='Pagination Device',
+            device_type=device_type,
+            role=device_role,
+            site=Site.objects.first(),
+        )
+        device.tags.set([
+            Tag.objects.create(name=f'Pagination Tag {index}', slug=f'pagination-tag-{index}')
+            for index in range(5)
+        ])
+
+        query = """
+        {
+            device_list(filters: {role: {slug: {exact: "pagination-role"}}}) {
+                tags(pagination: {offset: 1, limit: 2}) {
+                    slug
+                }
+            }
+        }
+        """
+        url = reverse('graphql')
+        response = self.client.post(url, data={'query': query}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        data = json.loads(response.content)
+        self.assertNotIn('errors', data)
+        self.assertEqual(
+            [tag['slug'] for tag in data['data']['device_list'][0]['tags']],
+            ['pagination-tag-1', 'pagination-tag-2'],
+        )
+
+        # MAX_PAGE_SIZE is enforced when no pagination is specified
+        query = """
+        {
+            device_list(filters: {role: {slug: {exact: "pagination-role"}}}) {
+                tags {
+                    slug
+                }
+            }
+        }
+        """
+        with override_settings(MAX_PAGE_SIZE=3):
+            response = self.client.post(url, data={'query': query}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        data = json.loads(response.content)
+        self.assertNotIn('errors', data)
+        self.assertEqual(len(data['data']['device_list'][0]['tags']), 3)
+
     def test_graphql_ip_address_list_assigned_object(self):
         """
         Requesting assigned_object should batch prefetch related objects.

+ 2 - 2
requirements.txt

@@ -37,8 +37,8 @@ rq==2.12.0
 social-auth-app-django==6.0.1
 social-auth-core==5.1.1
 sorl-thumbnail==13.1.0
-strawberry-graphql==0.327.7
-strawberry-graphql-django==0.90.0
+strawberry-graphql==0.334.2
+strawberry-graphql-django==0.93.0
 svgwrite==1.4.3
 tablib==3.10.0
 tzdata==2026.4