This commit is contained in:
parent
8b203e55a8
commit
3d3bebcb78
|
|
@ -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')],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
|
|
|
|||
Loading…
Reference in New Issue