Closes #22787: Improve GraphQL query efficiency when resolving assigned objects (#22792)

This commit is contained in:
Elliott Balsley 2026-08-04 21:28:20 +02:00 committed by GitHub
parent 8b203e55a8
commit 3d3bebcb78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 408 additions and 31 deletions

View File

@ -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')],

View File

@ -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

View File

@ -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')]

View File

@ -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

View File

@ -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')

View File

@ -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')]

View File

@ -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')]

View File

@ -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')]