From 3d3bebcb785ba89e188f313c4b224dd319714460 Mon Sep 17 00:00:00 2001 From: Elliott Balsley <3991046+llamafilm@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:28:20 +0200 Subject: [PATCH] Closes #22787: Improve GraphQL query efficiency when resolving assigned objects (#22792) --- netbox/circuits/graphql/types.py | 27 ++++- netbox/dcim/graphql/types.py | 101 +++++++++++++---- netbox/ipam/graphql/types.py | 72 +++++++++++- netbox/netbox/graphql/optimization.py | 43 +++++++ netbox/netbox/tests/test_graphql.py | 150 +++++++++++++++++++++++++ netbox/virtualization/graphql/types.py | 15 ++- netbox/vpn/graphql/types.py | 16 ++- netbox/wireless/graphql/types.py | 15 ++- 8 files changed, 408 insertions(+), 31 deletions(-) create mode 100644 netbox/netbox/graphql/optimization.py diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index d150d2b5c..65be61d7f 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -5,7 +5,9 @@ import strawberry_django from circuits import models from dcim.graphql.mixins import CabledObjectMixin +from dcim.models import Location, Region, Site, SiteGroup from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin +from netbox.graphql.optimization import build_gfk_prefetch from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType from tenancy.graphql.types import TenantType @@ -74,7 +76,19 @@ class ProviderNetworkType(PrimaryObjectType): class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): circuit: Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')] - @strawberry_django.field(prefetch_related='termination') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'termination', + [ + Location, + Region, + SiteGroup, + Site, + models.ProviderNetwork, + ], + ), + only=['termination_type', 'termination_id'], + ) def termination(self) -> Annotated[ Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')] @@ -133,7 +147,16 @@ class CircuitGroupType(OrganizationalObjectType): class CircuitGroupAssignmentType(TagsMixin, BaseObjectType): group: Annotated['CircuitGroupType', strawberry.lazy('circuits.graphql.types')] - @strawberry_django.field(prefetch_related='member') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'member', + [ + models.Circuit, + models.VirtualCircuit, + ], + ), + only=['member_type', 'member_id'], + ) def member(self) -> Annotated[ Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')] | Annotated['VirtualCircuitType', strawberry.lazy('circuits.graphql.types')], diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 7f3d01bcc..e4f4b7a14 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -9,6 +9,7 @@ from core.graphql.mixins import ChangelogMixin from dcim import models from extras.graphql.mixins import ConfigContextMixin, ContactsMixin, ImageAttachmentsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin +from netbox.graphql.optimization import build_gfk_prefetch from netbox.graphql.scalars import BigInt from netbox.graphql.types import ( BaseObjectType, @@ -19,7 +20,7 @@ from netbox.graphql.types import ( ) from users.graphql.mixins import OwnerMixin from utilities.querysets import RestrictedPrefetch -from virtualization.models import Cluster +from virtualization.models import Cluster, VMInterface from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin @@ -150,7 +151,25 @@ class CableBundleType(PrimaryObjectType): ) class CableTerminationType(NetBoxObjectType): cable: Annotated['CableType', strawberry.lazy('dcim.graphql.types')] | None - termination: Annotated[ + + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'termination', + [ + CircuitTermination, + models.ConsolePort, + models.ConsoleServerPort, + models.FrontPort, + models.Interface, + models.PowerFeed, + models.PowerOutlet, + models.PowerPort, + models.RearPort, + ], + ), + only=['termination_type', 'termination_id'], + ) + def termination(self) -> Annotated[ Annotated['CircuitTerminationType', strawberry.lazy('circuits.graphql.types')] | Annotated['ConsolePortType', strawberry.lazy('dcim.graphql.types')] | Annotated['ConsoleServerPortType', strawberry.lazy('dcim.graphql.types')] @@ -161,7 +180,8 @@ class CableTerminationType(NetBoxObjectType): | Annotated['PowerPortType', strawberry.lazy('dcim.graphql.types')] | Annotated['RearPortType', strawberry.lazy('dcim.graphql.types')], strawberry.union('CableTerminationTerminationType'), - ] | None + ] | None: + return self.termination @strawberry_django.type( @@ -330,22 +350,38 @@ class InventoryItemTemplateType(ComponentTemplateType): role: Annotated['InventoryItemRoleType', strawberry.lazy('dcim.graphql.types')] | None manufacturer: Annotated['ManufacturerType', strawberry.lazy('dcim.graphql.types')] - @strawberry_django.field(prefetch_related='parent') + @strawberry_django.field(prefetch_related='parent', only=['parent_id']) def parent(self) -> Annotated['InventoryItemTemplateType', strawberry.lazy('dcim.graphql.types')] | None: return self.parent child_items: list[Annotated['InventoryItemTemplateType', strawberry.lazy('dcim.graphql.types')]] - component: Annotated[ - Annotated['ConsolePortType', strawberry.lazy('dcim.graphql.types')] - | Annotated['ConsoleServerPortType', strawberry.lazy('dcim.graphql.types')] - | Annotated['FrontPortType', strawberry.lazy('dcim.graphql.types')] - | Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] - | Annotated['PowerOutletType', strawberry.lazy('dcim.graphql.types')] - | Annotated['PowerPortType', strawberry.lazy('dcim.graphql.types')] - | Annotated['RearPortType', strawberry.lazy('dcim.graphql.types')], + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'component', + [ + models.ConsolePortTemplate, + models.ConsoleServerPortTemplate, + models.FrontPortTemplate, + models.InterfaceTemplate, + models.PowerOutletTemplate, + models.PowerPortTemplate, + models.RearPortTemplate, + ], + ), + only=['component_type', 'component_id'], + ) + def component(self) -> Annotated[ + Annotated['ConsolePortTemplateType', strawberry.lazy('dcim.graphql.types')] + | Annotated['ConsoleServerPortTemplateType', strawberry.lazy('dcim.graphql.types')] + | Annotated['FrontPortTemplateType', strawberry.lazy('dcim.graphql.types')] + | Annotated['InterfaceTemplateType', strawberry.lazy('dcim.graphql.types')] + | Annotated['PowerOutletTemplateType', strawberry.lazy('dcim.graphql.types')] + | Annotated['PowerPortTemplateType', strawberry.lazy('dcim.graphql.types')] + | Annotated['RearPortTemplateType', strawberry.lazy('dcim.graphql.types')], strawberry.union('InventoryItemTemplateComponentType'), - ] | None + ] | None: + return self.component @strawberry_django.type( @@ -433,7 +469,16 @@ class FrontPortTemplateType(ModularComponentTemplateType): class MACAddressType(PrimaryObjectType): mac_address: str - @strawberry_django.field(prefetch_related='assigned_object') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'assigned_object', + [ + models.Interface, + VMInterface, + ], + ), + only=['assigned_object_type', 'assigned_object_id'], + ) def assigned_object(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VMInterfaceType', strawberry.lazy('virtualization.graphql.types')], @@ -497,11 +542,26 @@ class InventoryItemType(ComponentType): child_items: list[Annotated['InventoryItemType', strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field(prefetch_related='parent') + @strawberry_django.field(prefetch_related='parent', only=['parent_id']) def parent(self) -> Annotated['InventoryItemType', strawberry.lazy('dcim.graphql.types')] | None: return self.parent - component: Annotated[ + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'component', + [ + models.ConsolePort, + models.ConsoleServerPort, + models.FrontPort, + models.Interface, + models.PowerOutlet, + models.PowerPort, + models.RearPort, + ], + ), + only=['component_type', 'component_id'], + ) + def component(self) -> Annotated[ Annotated['ConsolePortType', strawberry.lazy('dcim.graphql.types')] | Annotated['ConsoleServerPortType', strawberry.lazy('dcim.graphql.types')] | Annotated['FrontPortType', strawberry.lazy('dcim.graphql.types')] @@ -510,7 +570,8 @@ class InventoryItemType(ComponentType): | Annotated['PowerPortType', strawberry.lazy('dcim.graphql.types')] | Annotated['RearPortType', strawberry.lazy('dcim.graphql.types')], strawberry.union('InventoryItemComponentType'), - ] | None + ] | None: + return self.component @strawberry_django.type( @@ -611,7 +672,7 @@ class ModuleBayType(ModularComponentType): installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None children: list[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field(prefetch_related='parent') + @strawberry_django.field(prefetch_related='parent', only=['parent_id']) def parent(self) -> Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -888,7 +949,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType): sites: list[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] children: list[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field(prefetch_related='parent') + @strawberry_django.field(prefetch_related='parent', only=['parent_id']) def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -965,7 +1026,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType): sites: list[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] children: list[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field(prefetch_related='parent') + @strawberry_django.field(prefetch_related='parent', only=['parent_id']) def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index b684022da..667f2142f 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -5,10 +5,13 @@ import strawberry_django from circuits.graphql.types import ProviderType from dcim.graphql.types import SiteType +from dcim.models import Device, Interface, Location, Rack, RackGroup, Region, Site, SiteGroup from extras.graphql.mixins import ContactsMixin from ipam import models +from netbox.graphql.optimization import build_gfk_prefetch from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType +from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface from .filters import * from .mixins import IPAddressesMixin @@ -18,6 +21,7 @@ if TYPE_CHECKING: DeviceType, InterfaceType, LocationType, + RackGroupType, RackType, RegionType, SiteGroupType, @@ -126,7 +130,16 @@ class FHRPGroupType(IPAddressesMixin, PrimaryObjectType): class FHRPGroupAssignmentType(BaseObjectType): group: Annotated['FHRPGroupType', strawberry.lazy('ipam.graphql.types')] - @strawberry_django.field(prefetch_related='interface') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'interface', + [ + Interface, + VMInterface, + ], + ), + only=['interface_type', 'interface_id'], + ) def interface(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VMInterfaceType', strawberry.lazy('virtualization.graphql.types')], @@ -155,7 +168,17 @@ class IPAddressType(ContactsMixin, PrimaryObjectType): def family(self) -> IPAddressFamilyType: return IPAddressFamilyType(value=self.family, label=f'IPv{self.family}') - @strawberry_django.field(prefetch_related='assigned_object') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'assigned_object', + [ + models.FHRPGroup, + Interface, + VMInterface, + ], + ), + only=['assigned_object_type', 'assigned_object_id'], + ) def assigned_object(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['FHRPGroupType', strawberry.lazy('ipam.graphql.types')] @@ -197,7 +220,18 @@ class PrefixType(ContactsMixin, PrimaryObjectType): def family(self) -> IPAddressFamilyType: return IPAddressFamilyType(value=self.family, label=f'IPv{self.family}') - @strawberry_django.field(prefetch_related='scope') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'scope', + [ + Region, + SiteGroup, + Site, + Location, + ], + ), + only=['scope_type', 'scope_id'], + ) def scope(self) -> Annotated[ Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')] @@ -259,7 +293,17 @@ class ServiceType(ContactsMixin, PrimaryObjectType): ports: list[int] ipaddresses: list[Annotated['IPAddressType', strawberry.lazy('ipam.graphql.types')]] - @strawberry_django.field(prefetch_related='parent') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'parent', + [ + Device, + VirtualMachine, + models.FHRPGroup, + ], + ), + only=['parent_object_type', 'parent_object_id'], + ) def parent(self) -> Annotated[ Annotated['DeviceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VirtualMachineType', strawberry.lazy('virtualization.graphql.types')] @@ -298,7 +342,7 @@ class VLANType(PrimaryObjectType): interfaces_as_tagged: list[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]] vminterfaces_as_tagged: list[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]] - @strawberry_django.field(prefetch_related='qinq_svlan') + @strawberry_django.field(prefetch_related='qinq_svlan', only=['qinq_svlan_id']) def qinq_svlan(self) -> Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None: return self.qinq_svlan @@ -316,11 +360,27 @@ class VLANGroupType(OrganizationalObjectType): total_vlan_ids: BigInt tenant: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None - @strawberry_django.field(prefetch_related='scope') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'scope', + [ + Cluster, + ClusterGroup, + Location, + Rack, + RackGroup, + Region, + Site, + SiteGroup, + ], + ), + only=['scope_type', 'scope_id'], + ) def scope(self) -> Annotated[ Annotated['ClusterType', strawberry.lazy('virtualization.graphql.types')] | Annotated['ClusterGroupType', strawberry.lazy('virtualization.graphql.types')] | Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] + | Annotated['RackGroupType', strawberry.lazy('dcim.graphql.types')] | Annotated['RackType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')] | Annotated['SiteType', strawberry.lazy('dcim.graphql.types')] diff --git a/netbox/netbox/graphql/optimization.py b/netbox/netbox/graphql/optimization.py new file mode 100644 index 000000000..d5263fae9 --- /dev/null +++ b/netbox/netbox/graphql/optimization.py @@ -0,0 +1,43 @@ +from collections.abc import Callable, Sequence + +from django.contrib.contenttypes.prefetch import GenericPrefetch +from django.db.models import Model, QuerySet +from strawberry.types import Info +from strawberry_django.optimizer import optimize +from strawberry_django.optimizer import optimizer as optimizer_ctx + +__all__ = ( + 'build_gfk_prefetch', + 'optimize_prefetch_queryset', +) + + +def optimize_prefetch_queryset(queryset: QuerySet, info: Info) -> QuerySet: + """ + Apply strawberry-django's query optimizer to a queryset used inside a GenericForeignKey prefetch. + """ + if ext := optimizer_ctx.get(): + return ext.optimize(queryset, info) + + return optimize(queryset, info) + + +def build_gfk_prefetch( + lookup: str, + models: Sequence[type[Model]], +) -> Callable[[Info], GenericPrefetch]: + """ + Return a selection-aware GenericPrefetch for a GenericForeignKey field. + + Each model gets its own queryset, optimized according to the client's GraphQL selection set. + """ + + def prefetch(info: Info) -> GenericPrefetch: + querysets = [ + optimize_prefetch_queryset(model.objects.all(), info) + for model in models + ] + + return GenericPrefetch(lookup, querysets) + + return prefetch diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 46f06dd6e..098e7d0e0 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -18,6 +18,7 @@ from dcim.models import ( Device, DeviceRole, DeviceType, + Interface, Location, Manufacturer, Rack, @@ -35,6 +36,12 @@ from utilities.tables import get_table_for_model from utilities.testing import APITestCase, APIViewTestCases, TestCase, disable_warnings +def count_primary_table_queries(queries, table): + """Count queries that read from `table` as the primary relation (not only as a join).""" + pattern = re.compile(rf'FROM "{re.escape(table)}"') + return sum(1 for query_record in queries if pattern.search(query_record['sql'])) + + class GraphQLTestCase(TestCase): def _schema_extension_instances(self): @@ -469,6 +476,149 @@ class GraphQLAPITestCase(APITestCase): msg=f'Expected batched tag prefetch, got {tag_queries} tag queries for 10 devices', ) + def test_graphql_ip_address_list_assigned_object(self): + """ + Requesting assigned_object should batch prefetch related objects. + """ + self.add_permissions('ipam.view_ipaddress', 'dcim.view_interface', 'dcim.view_device') + + site = Site.objects.first() + manufacturer = Manufacturer.objects.create(name='Assigned Object Manufacturer', slug='assigned-object-mfg') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, + model='Assigned Object Model', + slug='assigned-object-model', + ) + device_role = DeviceRole.objects.create(name='Assigned Object Role', slug='assigned-object-role') + device = Device.objects.create( + name='Assigned Object Device', + site=site, + device_type=device_type, + role=device_role, + ) + interface = Interface.objects.create(name='eth0', device=device, type='1000baset') + ip_addresses = IPAddress.objects.bulk_create([ + IPAddress(address=f'192.0.2.{index}/24', assigned_object=interface) + for index in range(1, 6) + ]) + ip_ids = json.dumps([str(ip.pk) for ip in ip_addresses]) + + query = f""" + {{ + ip_address_list(filters: {{id: {{in_list: {ip_ids}}}}}) {{ + address + assigned_object {{ + ... on InterfaceType {{ + name + device {{ + name + }} + }} + }} + }} + }} + """ + url = reverse('graphql') + + with CaptureQueriesContext(connection) as context: + 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']['ip_address_list']), len(ip_addresses)) + + device_queries = count_primary_table_queries(context.captured_queries, 'dcim_device') + self.assertLessEqual( + device_queries, + 2, + msg=f'Expected batched assigned_object prefetch, got {device_queries} device queries for 5 IP addresses', + ) + + def test_graphql_ip_address_list_assigned_object_nested_site(self): + """ + Nested assigned_object selections should be optimized on the GFK prefetch queryset. + """ + self.add_permissions( + 'ipam.view_ipaddress', + 'dcim.view_interface', + 'dcim.view_device', + 'dcim.view_site', + ) + + site = Site.objects.first() + manufacturer = Manufacturer.objects.create( + name='Nested Site Manufacturer', + slug='nested-site-mfg', + ) + device_type = DeviceType.objects.create( + manufacturer=manufacturer, + model='Nested Site Model', + slug='nested-site-model', + ) + device_role = DeviceRole.objects.create(name='Nested Site Role', slug='nested-site-role') + interfaces = [] + for index in range(5): + device = Device.objects.create( + name=f'Nested Site Device {index}', + site=site, + device_type=device_type, + role=device_role, + ) + interfaces.append(Interface.objects.create( + name=f'eth{index}', + device=device, + type='1000baset', + )) + ip_addresses = IPAddress.objects.bulk_create([ + IPAddress(address=f'192.0.2.{index}/24', assigned_object=interfaces[index - 1]) + for index in range(1, 6) + ]) + ip_ids = json.dumps([str(ip.pk) for ip in ip_addresses]) + + query = f""" + {{ + ip_address_list(filters: {{id: {{in_list: {ip_ids}}}}}) {{ + address + assigned_object {{ + ... on InterfaceType {{ + name + device {{ + name + site {{ + name + }} + }} + }} + }} + }} + }} + """ + url = reverse('graphql') + + with CaptureQueriesContext(connection) as context: + 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']['ip_address_list']), len(ip_addresses)) + for ip_data in data['data']['ip_address_list']: + self.assertEqual(ip_data['assigned_object']['device']['site']['name'], site.name) + + device_queries = count_primary_table_queries(context.captured_queries, 'dcim_device') + site_queries = count_primary_table_queries(context.captured_queries, 'dcim_site') + self.assertLessEqual( + device_queries, + 2, + msg=f'Expected batched device prefetch, got {device_queries} device queries for 5 IP addresses', + ) + self.assertLessEqual( + site_queries, + 2, + msg=f'Expected optimized site join, got {site_queries} site queries for 5 IP addresses', + ) + def test_offset_pagination(self): self.add_permissions('dcim.view_site') url = reverse('graphql') diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 81ef685a1..dbad15d54 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -3,8 +3,10 @@ from typing import TYPE_CHECKING, Annotated import strawberry import strawberry_django +from dcim.models import Location, Region, Site, SiteGroup from extras.graphql.mixins import ConfigContextMixin, ContactsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin +from netbox.graphql.optimization import build_gfk_prefetch from netbox.graphql.scalars import BigInt from netbox.graphql.types import NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType from users.graphql.mixins import OwnerMixin @@ -59,7 +61,18 @@ class ClusterType(ContactsMixin, VLANGroupsMixin, PrimaryObjectType): virtual_machines: list[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]] devices: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field(prefetch_related='scope') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'scope', + [ + Region, + SiteGroup, + Site, + Location, + ], + ), + only=['scope_type', 'scope_id'], + ) def scope(self) -> Annotated[ Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')] diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 40f125e83..1d7216599 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -3,8 +3,12 @@ from typing import TYPE_CHECKING, Annotated import strawberry import strawberry_django +from dcim.models import Interface from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin +from ipam.models import VLAN +from netbox.graphql.optimization import build_gfk_prefetch from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType +from virtualization.models import VMInterface from vpn import models from .filters import * @@ -145,7 +149,17 @@ class L2VPNType(ContactsMixin, PrimaryObjectType): class L2VPNTerminationType(NetBoxObjectType): l2vpn: Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')] - @strawberry_django.field(prefetch_related='assigned_object') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'assigned_object', + [ + Interface, + VMInterface, + VLAN, + ], + ), + only=['assigned_object_type', 'assigned_object_id'], + ) def assigned_object(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VLANType', strawberry.lazy('ipam.graphql.types')] diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 557f32ce4..589fc237c 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, Annotated import strawberry import strawberry_django +from dcim.models import Location, Region, Site, SiteGroup +from netbox.graphql.optimization import build_gfk_prefetch from netbox.graphql.types import NestedGroupObjectType, PrimaryObjectType from wireless import models @@ -46,7 +48,18 @@ class WirelessLANType(PrimaryObjectType): interfaces: list[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field(prefetch_related='scope') + @strawberry_django.field( + prefetch_related=build_gfk_prefetch( + 'scope', + [ + Region, + SiteGroup, + Site, + Location, + ], + ), + only=['scope_type', 'scope_id'], + ) def scope(self) -> Annotated[ Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')]