Closes #22721: Enable plugins to extend core GraphQL API

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jeremy Stretch 2026-07-22 13:16:43 -04:00
parent 0a3e9e3bdb
commit a24fbb06ce
30 changed files with 926 additions and 281 deletions

View File

@ -25,9 +25,9 @@ class MyModelType:
@strawberry.type
class MyQuery:
@strawberry.field
def dummymodel(self, id: int) -> DummyModelType:
def mymodel(self, id: int) -> MyModelType:
return None
dummymodel_list: list[DummyModelType] = strawberry_django.field()
mymodel_list: list[MyModelType] = strawberry_django.field()
schema = [
@ -35,6 +35,94 @@ schema = [
]
```
## Extending Core Types & Filters
!!! info "This feature was introduced in NetBox v4.6."
In addition to registering its own top-level query fields, a plugin can inject fields and filters onto NetBox's **existing** core GraphQL types (e.g. `DeviceType`). This allows a plugin's related data to be traversed within a single query rooted at a core object, rather than requiring a separate top-level query. This mirrors the `PluginTemplateExtension` mechanism used to extend core object views in the UI.
An extension is a mixin class declaring a `models` attribute: a list of the lowercased `app_label.model` labels of the core types it extends. Output-type extensions are collected from `graphql.type_extensions` and filter extensions from `graphql.filter_extensions` by default; these paths can be overridden via the `graphql_type_extensions` and `graphql_filter_extensions` attributes on the PluginConfig.
Each declared path must resolve to a list named `type_extensions` (or `filter_extensions`) - for example, defined in `graphql.py` alongside the schema, or re-exported from the plugin's `graphql` package.
!!! warning
Do not import core GraphQL modules (e.g. `dcim.graphql.types`) from a plugin's `ready()`. Doing so assembles the affected core types before other plugins have registered their extensions, which are then silently dropped. A warning is logged under `netbox.graphql` if this occurs.
### Type Extensions
An output-type extension is a `@strawberry.type` class whose fields and resolvers are spliced into the target type:
```python
# graphql.py (or graphql/type_extensions.py)
from typing import Annotated
import strawberry
import strawberry_django
from utilities.querysets import RestrictedPrefetch
from my_plugin.models import Widget
@strawberry.type
class DeviceTypeExtension:
models = ['dcim.device']
@strawberry_django.field(
prefetch_related=lambda info: RestrictedPrefetch(
'widgets', info.context.request.user, 'view', queryset=Widget.objects.all()
),
)
def widgets(self) -> list[Annotated['WidgetType', strawberry.lazy('my_plugin.graphql.types')]]:
return self.widgets.all()
type_extensions = [
DeviceTypeExtension,
]
```
!!! note
Scope any related-object resolver with `RestrictedPrefetch(..., info.context.request.user, 'view', ...)`, as shown above. Object permissions are only applied to the top-level queryset, so a plain `prefetch_related='widgets'` returns related objects the requesting user may not be permitted to see.
### Filter Extensions
A filter extension is a `@strawberry.type` class declaring additional filters - either as annotated filter fields or as custom filter methods - which are spliced into the target filter:
```python
# graphql.py (or graphql/filter_extensions.py)
import strawberry
import strawberry_django
from django.db.models import Q
@strawberry.type
class DeviceFilterExtension:
models = ['dcim.device']
@strawberry_django.filter_field()
def has_widgets(self, value: bool, prefix) -> Q:
return Q(**{f'{prefix}widgets__isnull': not value})
filter_extensions = [
DeviceFilterExtension,
]
```
With both registered, a client can fetch a device and its plugin-provided data in a single query:
```graphql
query {
device_list(filters: { has_widgets: true }) {
name
widgets { id name }
}
}
```
!!! note
Extensions are strictly additive: they can only add new fields, never replace existing ones. If an extension declares a name the core type already provides, the core definition always takes precedence and the extension's version is ignored. If two extensions on the same type declare the same new name, the one whose plugin is loaded first (earlier in `PLUGINS`) wins. Both cases are logged as warnings under the `netbox.graphql` logger.
## GraphQL Objects
NetBox provides two object type classes for use by plugins.

View File

@ -123,6 +123,8 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
| `menu` | The dotted path to a top-level navigation menu provided by the plugin (default: `navigation.menu`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `graphql_type_extensions` | The dotted path to the list of GraphQL output-type extension classes, if any (default: `graphql.type_extensions`) |
| `graphql_filter_extensions` | The dotted path to the list of GraphQL filter extension classes, if any (default: `graphql.filter_extensions`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.

View File

@ -10,7 +10,12 @@ from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import DistanceFilterMixin, ImageAttachmentFilterMixin
from netbox.graphql.filters import ChangeLoggedModelFilter, OrganizationalModelFilter, PrimaryModelFilter
from netbox.graphql.filters import (
ChangeLoggedModelFilter,
OrganizationalModelFilter,
PrimaryModelFilter,
register_filter,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
if TYPE_CHECKING:
@ -36,7 +41,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
@register_filter(models.CircuitTermination, lookups=True)
class CircuitTerminationFilter(
CustomFieldsFilterMixin,
TagsFilterMixin,
@ -83,7 +88,7 @@ class CircuitTerminationFilter(
)
@strawberry_django.filter_type(models.Circuit, lookups=True)
@register_filter(models.Circuit, lookups=True)
class CircuitFilter(
ContactFilterMixin,
ImageAttachmentFilterMixin,
@ -117,17 +122,17 @@ class CircuitFilter(
)
@strawberry_django.filter_type(models.CircuitType, lookups=True)
@register_filter(models.CircuitType, lookups=True)
class CircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
@register_filter(models.CircuitGroup, lookups=True)
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
@register_filter(models.CircuitGroupAssignment, lookups=True)
class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -142,7 +147,7 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
)
@strawberry_django.filter_type(models.Provider, lookups=True)
@register_filter(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -152,7 +157,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
@register_filter(models.ProviderAccount, lookups=True)
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -162,7 +167,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
@register_filter(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -172,12 +177,12 @@ class ProviderNetworkFilter(PrimaryModelFilter):
service_id: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
@register_filter(models.VirtualCircuitType, lookups=True)
class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
@register_filter(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
cid: StrFilterLookup | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -200,7 +205,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
@register_filter(models.VirtualCircuitTermination, lookups=True)
class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()

View File

@ -6,7 +6,7 @@ import strawberry_django
from circuits import models
from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType, register_type
from tenancy.graphql.types import TenantType
from .filters import *
@ -30,7 +30,7 @@ __all__ = (
)
@strawberry_django.type(
@register_type(
models.Provider,
fields='__all__',
filters=ProviderFilter,
@ -43,7 +43,7 @@ class ProviderType(ContactsMixin, PrimaryObjectType):
accounts: list[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ProviderAccount,
fields='__all__',
filters=ProviderAccountFilter,
@ -54,7 +54,7 @@ class ProviderAccountType(ContactsMixin, PrimaryObjectType):
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ProviderNetwork,
fields='__all__',
filters=ProviderNetworkFilter,
@ -65,7 +65,7 @@ class ProviderNetworkType(PrimaryObjectType):
circuit_terminations: list[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@register_type(
models.CircuitTermination,
exclude=['termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'],
filters=CircuitTerminationFilter,
@ -86,7 +86,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
return self.termination
@strawberry_django.type(
@register_type(
models.CircuitType,
fields='__all__',
filters=CircuitTypeFilter,
@ -98,7 +98,7 @@ class CircuitTypeType(OrganizationalObjectType):
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Circuit,
fields='__all__',
filters=CircuitFilter,
@ -114,7 +114,7 @@ class CircuitType(PrimaryObjectType, ContactsMixin):
terminations: list[CircuitTerminationType]
@strawberry_django.type(
@register_type(
models.CircuitGroup,
fields='__all__',
filters=CircuitGroupFilter,
@ -124,7 +124,7 @@ class CircuitGroupType(OrganizationalObjectType):
tenant: TenantType | None
@strawberry_django.type(
@register_type(
models.CircuitGroupAssignment,
exclude=['member_type', 'member_id'],
filters=CircuitGroupAssignmentFilter,
@ -142,7 +142,7 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
return self.member
@strawberry_django.type(
@register_type(
models.VirtualCircuitType,
fields='__all__',
filters=VirtualCircuitTypeFilter,
@ -154,7 +154,7 @@ class VirtualCircuitTypeType(OrganizationalObjectType):
virtual_circuits: list[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@register_type(
models.VirtualCircuitTermination,
fields='__all__',
filters=VirtualCircuitTerminationFilter,
@ -171,7 +171,7 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
] = strawberry_django.field(select_related=["interface"])
@strawberry_django.type(
@register_type(
models.VirtualCircuit,
fields='__all__',
filters=VirtualCircuitFilter,

View File

@ -7,7 +7,7 @@ from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
from core import models
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter, register_filter
from .enums import *
@ -23,7 +23,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.DataFile, lookups=True)
@register_filter(models.DataFile, lookups=True)
class DataFileFilter(BaseModelFilter):
created: DatetimeFilterLookup | None = strawberry_django.filter_field()
last_updated: DatetimeFilterLookup | None = strawberry_django.filter_field()
@ -38,7 +38,7 @@ class DataFileFilter(BaseModelFilter):
hash: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DataSource, lookups=True)
@register_filter(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
type: StrFilterLookup | None = strawberry_django.filter_field()
@ -57,7 +57,7 @@ class DataSourceFilter(PrimaryModelFilter):
)
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
@register_filter(models.ObjectChange, lookups=True)
class ObjectChangeFilter(BaseModelFilter):
time: DatetimeFilterLookup | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@ -84,7 +84,7 @@ class ObjectChangeFilter(BaseModelFilter):
)
@strawberry_django.filter_type(DjangoContentType, lookups=True)
@register_filter(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseModelFilter):
app_label: StrFilterLookup | None = strawberry_django.filter_field()
model: StrFilterLookup | None = strawberry_django.filter_field()

View File

@ -1,11 +1,10 @@
from typing import Annotated
import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType
from core import models
from netbox.graphql.types import BaseObjectType, PrimaryObjectType
from netbox.graphql.types import BaseObjectType, PrimaryObjectType, register_type
from .filters import *
@ -17,7 +16,7 @@ __all__ = (
)
@strawberry_django.type(
@register_type(
models.DataFile,
exclude=['data',],
filters=DataFileFilter,
@ -27,7 +26,7 @@ class DataFileType(BaseObjectType):
source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')]
@strawberry_django.type(
@register_type(
models.DataSource,
fields='__all__',
filters=DataSourceFilter,
@ -37,7 +36,7 @@ class DataSourceType(PrimaryObjectType):
datafiles: list[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ObjectChange,
fields='__all__',
filters=ObjectChangeFilter,
@ -47,7 +46,7 @@ class ObjectChangeType(BaseObjectType):
pass
@strawberry_django.type(
@register_type(
DjangoContentType,
fields='__all__',
pagination=True

View File

@ -31,6 +31,7 @@ from netbox.graphql.filters import (
NetBoxModelFilter,
OrganizationalModelFilter,
PrimaryModelFilter,
register_filter,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface
@ -121,12 +122,12 @@ __all__ = (
)
@strawberry_django.filter_type(models.CableBundle, lookups=True)
@register_filter(models.CableBundle, lookups=True)
class CableBundleFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Cable, lookups=True)
@register_filter(models.Cable, lookups=True)
class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
type: BaseFilterLookup[Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -149,7 +150,7 @@ class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.CableTermination, lookups=True)
@register_filter(models.CableTermination, lookups=True)
class CableTerminationFilter(ChangeLoggedModelFilter):
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
cable_id: ID | None = strawberry_django.filter_field()
@ -176,7 +177,7 @@ class CableTerminationFilter(ChangeLoggedModelFilter):
)
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
@register_filter(models.ConsolePort, lookups=True)
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -186,14 +187,14 @@ class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
)
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
@register_filter(models.ConsolePortTemplate, lookups=True)
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
@register_filter(models.ConsoleServerPort, lookups=True)
class ConsoleServerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -203,14 +204,14 @@ class ConsoleServerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilt
)
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
@register_filter(models.ConsoleServerPortTemplate, lookups=True)
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.Device, lookups=True)
@register_filter(models.Device, lookups=True)
class DeviceFilter(
ContactFilterMixin,
TenancyFilterMixin,
@ -329,7 +330,7 @@ class DeviceFilter(
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
@register_filter(models.DeviceBay, lookups=True)
class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@ -338,12 +339,12 @@ class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
installed_device_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
@register_filter(models.DeviceBayTemplate, lookups=True)
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
@register_filter(models.InventoryItemTemplate, lookups=True)
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -363,7 +364,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedMode
part_id: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
@register_filter(models.DeviceRole, lookups=True)
class DeviceRoleFilter(RenderConfigFilterMixin, OrganizationalModelFilter):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -371,7 +372,7 @@ class DeviceRoleFilter(RenderConfigFilterMixin, OrganizationalModelFilter):
vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceType, lookups=True)
@register_filter(models.DeviceType, lookups=True)
class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -448,7 +449,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
device_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.FrontPort, lookups=True)
@register_filter(models.FrontPort, lookups=True)
class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -458,7 +459,7 @@ class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
)
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
@register_filter(models.FrontPortTemplate, lookups=True)
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -468,7 +469,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
)
@strawberry_django.filter_type(models.PortMapping, lookups=True)
@register_filter(models.PortMapping, lookups=True)
class PortMappingFilter(BaseModelFilter):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
front_port: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@ -481,7 +482,7 @@ class PortMappingFilter(BaseModelFilter):
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PortTemplateMapping, lookups=True)
@register_filter(models.PortTemplateMapping, lookups=True)
class PortTemplateMappingFilter(BaseModelFilter):
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -499,7 +500,7 @@ class PortTemplateMappingFilter(BaseModelFilter):
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.MACAddress, lookups=True)
@register_filter(models.MACAddress, lookups=True)
class MACAddressFilter(PrimaryModelFilter):
mac_address: StrFilterLookup | None = strawberry_django.filter_field()
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
@ -525,7 +526,7 @@ class MACAddressFilter(PrimaryModelFilter):
return ~Q(query)
@strawberry_django.filter_type(models.Interface, lookups=True)
@register_filter(models.Interface, lookups=True)
class InterfaceFilter(
ModularComponentFilterMixin,
InterfaceBaseFilterMixin,
@ -630,7 +631,7 @@ class InterfaceFilter(
return queryset, Q()
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
@register_filter(models.InterfaceTemplate, lookups=True)
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -662,7 +663,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
)
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
@register_filter(models.InventoryItem, lookups=True)
class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -689,14 +690,14 @@ class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
@register_filter(models.InventoryItemRole, lookups=True)
class InventoryItemRoleFilter(OrganizationalModelFilter):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.Location, lookups=True)
@register_filter(models.Location, lookups=True)
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilter):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@ -712,12 +713,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
)
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
@register_filter(models.Manufacturer, lookups=True)
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.Module, lookups=True)
@register_filter(models.Module, lookups=True)
class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()
@ -766,7 +767,7 @@ class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
@register_filter(models.ModuleBay, lookups=True)
class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -780,7 +781,7 @@ class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
module_bay_type_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
@register_filter(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
position: StrFilterLookup | None = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@ -790,7 +791,7 @@ class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
module_bay_type_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleBayType, lookups=True)
@register_filter(models.ModuleBayType, lookups=True)
class ModuleBayTypeFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -800,12 +801,12 @@ class ModuleBayTypeFilter(PrimaryModelFilter):
manufacturer_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
@register_filter(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleType, lookups=True)
@register_filter(models.ModuleType, lookups=True)
class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -858,7 +859,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
module_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Platform, lookups=True)
@register_filter(models.Platform, lookups=True)
class PlatformFilter(OrganizationalModelFilter):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -870,7 +871,7 @@ class PlatformFilter(OrganizationalModelFilter):
config_template_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
@register_filter(models.PowerFeed, lookups=True)
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -905,7 +906,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
)
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
@register_filter(models.PowerOutlet, lookups=True)
class PowerOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -925,7 +926,7 @@ class PowerOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
)
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
@register_filter(models.PowerOutletTemplate, lookups=True)
class PowerOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -939,7 +940,7 @@ class PowerOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLogge
)
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
@register_filter(models.PowerPanel, lookups=True)
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilter):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@ -952,7 +953,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PowerPort, lookups=True)
@register_filter(models.PowerPort, lookups=True)
class PowerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -965,7 +966,7 @@ class PowerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
)
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
@register_filter(models.PowerPortTemplate, lookups=True)
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -978,7 +979,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
)
@strawberry_django.filter_type(models.RackType, lookups=True)
@register_filter(models.RackType, lookups=True)
class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -993,7 +994,7 @@ class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMi
rack_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Rack, lookups=True)
@register_filter(models.Rack, lookups=True)
class RackFilter(
ContactFilterMixin,
ImageAttachmentFilterMixin,
@ -1038,12 +1039,12 @@ class RackFilter(
)
@strawberry_django.filter_type(models.RackGroup, lookups=True)
@register_filter(models.RackGroup, lookups=True)
class RackGroupFilter(OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.RackReservation, lookups=True)
@register_filter(models.RackReservation, lookups=True)
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field()
@ -1059,14 +1060,14 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.RackRole, lookups=True)
@register_filter(models.RackRole, lookups=True)
class RackRoleFilter(OrganizationalModelFilter):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.RearPort, lookups=True)
@register_filter(models.RearPort, lookups=True)
class RearPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -1079,7 +1080,7 @@ class RearPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
)
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
@register_filter(models.RearPortTemplate, lookups=True)
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -1092,7 +1093,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedMo
)
@strawberry_django.filter_type(models.Region, lookups=True)
@register_filter(models.Region, lookups=True)
class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -1102,7 +1103,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
)
@strawberry_django.filter_type(models.Site, lookups=True)
@register_filter(models.Site, lookups=True)
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -1138,7 +1139,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
)
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
@register_filter(models.SiteGroup, lookups=True)
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -1148,7 +1149,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
)
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
@register_filter(models.VirtualChassis, lookups=True)
class VirtualChassisFilter(PrimaryModelFilter):
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
master_id: ID | None = strawberry_django.filter_field()
@ -1160,7 +1161,7 @@ class VirtualChassisFilter(PrimaryModelFilter):
member_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
@register_filter(models.VirtualDeviceContext, lookups=True)
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()

View File

@ -17,6 +17,7 @@ from netbox.graphql.types import (
NetBoxObjectType,
OrganizationalObjectType,
PrimaryObjectType,
register_type,
)
from users.graphql.mixins import OwnerMixin
from utilities.querysets import RestrictedPrefetch
@ -134,7 +135,7 @@ class ModularComponentTemplateType(ComponentTemplateType):
#
@strawberry_django.type(
@register_type(
models.CableBundle,
fields='__all__',
filters=CableBundleFilter,
@ -144,7 +145,7 @@ class CableBundleType(PrimaryObjectType):
cables: list[Annotated['CableType', strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.CableTermination,
exclude=['termination_type', 'termination_id', '_device', '_rack', '_location', '_site'],
filters=CableTerminationFilter,
@ -166,7 +167,7 @@ class CableTerminationType(NetBoxObjectType):
] | None
@strawberry_django.type(
@register_type(
models.Cable,
fields='__all__',
filters=CableFilter,
@ -206,7 +207,7 @@ class CableType(PrimaryObjectType):
]]
@strawberry_django.type(
@register_type(
models.ConsolePort,
exclude=['_path'],
filters=ConsolePortFilter,
@ -216,7 +217,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
pass
@strawberry_django.type(
@register_type(
models.ConsolePortTemplate,
fields='__all__',
filters=ConsolePortTemplateFilter,
@ -226,7 +227,7 @@ class ConsolePortTemplateType(ModularComponentTemplateType):
pass
@strawberry_django.type(
@register_type(
models.ConsoleServerPort,
exclude=['_path'],
filters=ConsoleServerPortFilter,
@ -236,7 +237,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
pass
@strawberry_django.type(
@register_type(
models.ConsoleServerPortTemplate,
fields='__all__',
filters=ConsoleServerPortTemplateFilter,
@ -246,7 +247,7 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
pass
@strawberry_django.type(
@register_type(
models.Device,
fields='__all__',
filters=DeviceFilter,
@ -302,7 +303,7 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, Prima
return self.parent_bay if hasattr(self, 'parent_bay') else None
@strawberry_django.type(
@register_type(
models.DeviceBay,
fields='__all__',
filters=DeviceBayFilter,
@ -312,7 +313,7 @@ class DeviceBayType(ComponentType):
installed_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.DeviceBayTemplate,
fields='__all__',
filters=DeviceBayTemplateFilter,
@ -322,7 +323,7 @@ class DeviceBayTemplateType(ComponentTemplateType):
pass
@strawberry_django.type(
@register_type(
models.InventoryItemTemplate,
exclude=['component_type', 'component_id', 'parent', 'path'],
filters=InventoryItemTemplateFilter,
@ -350,7 +351,7 @@ class InventoryItemTemplateType(LtreeNodeMixin, ComponentTemplateType):
] | None
@strawberry_django.type(
@register_type(
models.DeviceRole,
exclude=['path', 'sort_path'],
filters=DeviceRoleFilter,
@ -366,7 +367,7 @@ class DeviceRoleType(NestedLtreeGroupObjectType):
devices: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.DeviceType,
fields='__all__',
filters=DeviceTypeFilter,
@ -402,7 +403,7 @@ class DeviceTypeType(PrimaryObjectType):
consoleporttemplates: list[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.FrontPort,
fields='__all__',
filters=FrontPortFilter,
@ -414,7 +415,7 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
mappings: list[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.FrontPortTemplate,
fields='__all__',
filters=FrontPortTemplateFilter,
@ -426,7 +427,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
mappings: list[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.MACAddress,
exclude=['assigned_object_type', 'assigned_object_id'],
filters=MACAddressFilter,
@ -444,7 +445,7 @@ class MACAddressType(PrimaryObjectType):
return self.assigned_object
@strawberry_django.type(
@register_type(
models.Interface,
exclude=['_path'],
filters=InterfaceFilter,
@ -474,7 +475,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
mac_addresses: list[Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.InterfaceTemplate,
fields='__all__',
filters=InterfaceTemplateFilter,
@ -489,7 +490,7 @@ class InterfaceTemplateType(ModularComponentTemplateType):
child_interfaces: list[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.InventoryItem,
exclude=['component_type', 'component_id', 'parent', 'path'],
filters=InventoryItemFilter,
@ -517,7 +518,7 @@ class InventoryItemType(LtreeNodeMixin, ComponentType):
] | None
@strawberry_django.type(
@register_type(
models.InventoryItemRole,
fields='__all__',
filters=InventoryItemRoleFilter,
@ -530,7 +531,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
inventory_item_templates: list[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Location,
# fields='__all__',
exclude=['parent', 'path', 'sort_path'], # bug - temp
@ -568,7 +569,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Nested
return self.circuit_terminations.all()
@strawberry_django.type(
@register_type(
models.Manufacturer,
fields='__all__',
filters=ManufacturerFilter,
@ -583,7 +584,7 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin):
module_types: list[Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Module,
fields='__all__',
filters=ModuleFilter,
@ -603,7 +604,7 @@ class ModuleType(PrimaryObjectType):
frontports: list[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ModuleBay,
# fields='__all__',
exclude=['parent', 'path', 'sort_path'],
@ -621,7 +622,7 @@ class ModuleBayType(LtreeNodeMixin, ModularComponentType):
return self.parent
@strawberry_django.type(
@register_type(
models.ModuleBayTemplate,
fields='__all__',
filters=ModuleBayTemplateFilter,
@ -631,7 +632,7 @@ class ModuleBayTemplateType(ModularComponentTemplateType):
module_bay_types: list[Annotated["ModuleBayTypeType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ModuleBayType,
fields='__all__',
filters=ModuleBayTypeFilter,
@ -645,7 +646,7 @@ class ModuleBayTypeType(PrimaryObjectType):
module_bay_templates: list[Annotated["ModuleBayTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ModuleTypeProfile,
fields='__all__',
filters=ModuleTypeProfileFilter,
@ -655,7 +656,7 @@ class ModuleTypeProfileType(PrimaryObjectType):
module_types: list[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ModuleType,
fields='__all__',
filters=ModuleTypeFilter,
@ -685,7 +686,7 @@ class ModuleTypeType(PrimaryObjectType):
consoleporttemplates: list[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Platform,
exclude=['path', 'sort_path'],
filters=PlatformFilter,
@ -701,7 +702,7 @@ class PlatformType(NestedLtreeGroupObjectType):
devices: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.PortMapping,
fields='__all__',
filters=PortMappingFilter,
@ -712,7 +713,7 @@ class PortMappingType(ModularComponentTemplateType):
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
@register_type(
models.PortTemplateMapping,
fields='__all__',
filters=PortTemplateMappingFilter,
@ -723,7 +724,7 @@ class PortMappingTemplateType(ModularComponentTemplateType):
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
@register_type(
models.PowerFeed,
exclude=['_path'],
filters=PowerFeedFilter,
@ -735,7 +736,7 @@ class PowerFeedType(CabledObjectMixin, PathEndpointMixin, PrimaryObjectType):
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.PowerOutlet,
exclude=['_path'],
filters=PowerOutletFilter,
@ -746,7 +747,7 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
color: str
@strawberry_django.type(
@register_type(
models.PowerOutletTemplate,
fields='__all__',
filters=PowerOutletTemplateFilter,
@ -757,7 +758,7 @@ class PowerOutletTemplateType(ModularComponentTemplateType):
color: str
@strawberry_django.type(
@register_type(
models.PowerPanel,
fields='__all__',
filters=PowerPanelFilter,
@ -770,7 +771,7 @@ class PowerPanelType(ContactsMixin, PrimaryObjectType):
powerfeeds: list[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.PowerPort,
exclude=['_path'],
filters=PowerPortFilter,
@ -781,7 +782,7 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
poweroutlets: list[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.PowerPortTemplate,
fields='__all__',
filters=PowerPortTemplateFilter,
@ -791,7 +792,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
poweroutlet_templates: list[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.RackGroup,
fields='__all__',
filters=RackGroupFilter,
@ -802,7 +803,7 @@ class RackGroupType(OrganizationalObjectType):
racks: list[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.RackType,
fields='__all__',
filters=RackTypeFilter,
@ -813,7 +814,7 @@ class RackTypeType(ImageAttachmentsMixin, PrimaryObjectType):
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
@register_type(
models.Rack,
fields='__all__',
filters=RackFilter,
@ -833,7 +834,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObj
cabletermination_set: list[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.RackReservation,
fields='__all__',
filters=RackReservationFilter,
@ -857,7 +858,7 @@ class RackReservationType(PrimaryObjectType):
return len(self.units)
@strawberry_django.type(
@register_type(
models.RackRole,
fields='__all__',
filters=RackRoleFilter,
@ -869,7 +870,7 @@ class RackRoleType(OrganizationalObjectType):
racks: list[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.RearPort,
fields='__all__',
filters=RearPortFilter,
@ -881,7 +882,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
mappings: list[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.RearPortTemplate,
fields='__all__',
filters=RearPortTemplateFilter,
@ -893,7 +894,7 @@ class RearPortTemplateType(ModularComponentTemplateType):
mappings: list[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Region,
exclude=['parent', 'path', 'sort_path'],
filters=RegionFilter,
@ -928,7 +929,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, NestedLtreeGroupObjectType):
return self.circuit_terminations.all()
@strawberry_django.type(
@register_type(
models.Site,
fields='__all__',
filters=SiteFilter,
@ -970,7 +971,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObj
return self.circuit_terminations.all()
@strawberry_django.type(
@register_type(
models.SiteGroup,
exclude=['parent', 'path', 'sort_path'], # bug - temp
filters=SiteGroupFilter,
@ -1005,7 +1006,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedLtreeGroupObjectType):
return self.circuit_terminations.all()
@strawberry_django.type(
@register_type(
models.VirtualChassis,
fields='__all__',
filters=VirtualChassisFilter,
@ -1018,7 +1019,7 @@ class VirtualChassisType(PrimaryObjectType):
members: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.VirtualDeviceContext,
fields='__all__',
filters=VirtualDeviceContextFilter,

View File

@ -9,7 +9,7 @@ from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLook
from extras import models
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import SyncedDataFilterMixin
from netbox.graphql.filters import BaseModelFilter, ChangeLoggedModelFilter, PrimaryModelFilter
from netbox.graphql.filters import BaseModelFilter, ChangeLoggedModelFilter, PrimaryModelFilter, register_filter
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
@ -59,7 +59,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
@register_filter(models.ConfigContext, lookups=True)
class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@ -116,14 +116,14 @@ class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
)
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
@register_filter(models.ConfigContextProfile, lookups=True)
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
@register_filter(models.ConfigTemplate, lookups=True)
class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -137,7 +137,7 @@ class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.CustomField, lookups=True)
@register_filter(models.CustomField, lookups=True)
class CustomFieldFilter(ChangeLoggedModelFilter):
type: BaseFilterLookup[Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
strawberry_django.filter_field()
@ -197,7 +197,7 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
comments: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
@register_filter(models.CustomFieldChoiceSet, lookups=True)
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -239,7 +239,7 @@ class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
return queryset, params
@strawberry_django.filter_type(models.CustomLink, lookups=True)
@register_filter(models.CustomLink, lookups=True)
class CustomLinkFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@ -257,7 +257,7 @@ class CustomLinkFilter(ChangeLoggedModelFilter):
new_window: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
@register_filter(models.ExportTemplate, lookups=True)
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -271,7 +271,7 @@ class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ImageAttachment, lookups=True)
@register_filter(models.ImageAttachment, lookups=True)
class ImageAttachmentFilter(ChangeLoggedModelFilter):
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -289,7 +289,7 @@ class ImageAttachmentFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
@register_filter(models.JournalEntry, lookups=True)
class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -305,7 +305,7 @@ class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedM
comments: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Notification, lookups=True)
@register_filter(models.Notification, lookups=True)
class NotificationFilter(BaseModelFilter):
created: DatetimeFilterLookup | None = strawberry_django.filter_field()
read: DatetimeFilterLookup | None = strawberry_django.filter_field()
@ -320,7 +320,7 @@ class NotificationFilter(BaseModelFilter):
event_type: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
@register_filter(models.NotificationGroup, lookups=True)
class NotificationGroupFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -328,7 +328,7 @@ class NotificationGroupFilter(ChangeLoggedModelFilter):
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
@register_filter(models.SavedFilter, lookups=True)
class SavedFilterFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -345,7 +345,7 @@ class SavedFilterFilter(ChangeLoggedModelFilter):
)
@strawberry_django.filter_type(models.Subscription, lookups=True)
@register_filter(models.Subscription, lookups=True)
class SubscriptionFilter(BaseModelFilter):
created: DatetimeFilterLookup | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@ -357,7 +357,7 @@ class SubscriptionFilter(BaseModelFilter):
object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.TableConfig, lookups=True)
@register_filter(models.TableConfig, lookups=True)
class TableConfigFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -370,7 +370,7 @@ class TableConfigFilter(ChangeLoggedModelFilter):
shared: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Tag, lookups=True)
@register_filter(models.Tag, lookups=True)
class TagFilter(ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -380,7 +380,7 @@ class TagFilter(ChangeLoggedModelFilter):
description: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Webhook, lookups=True)
@register_filter(models.Webhook, lookups=True)
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -401,7 +401,7 @@ class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelF
)
@strawberry_django.filter_type(models.EventRule, lookups=True)
@register_filter(models.EventRule, lookups=True)
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()

View File

@ -1,14 +1,13 @@
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
from strawberry.scalars import JSON
from strawberry.types import Info
from core.graphql.mixins import SyncedDataMixin
from extras import models
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, PrimaryObjectType
from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, PrimaryObjectType, register_type
from users.graphql.mixins import OwnerMixin
from .filters import *
@ -60,7 +59,7 @@ class SharedObjectMixin:
return queryset.restrict_to_shared(info.context.request.user)
@strawberry_django.type(
@register_type(
models.ConfigContextProfile,
fields='__all__',
filters=ConfigContextProfileFilter,
@ -70,7 +69,7 @@ class ConfigContextProfileType(SyncedDataMixin, PrimaryObjectType):
pass
@strawberry_django.type(
@register_type(
models.ConfigContext,
fields='__all__',
filters=ConfigContextFilter,
@ -93,7 +92,7 @@ class ConfigContextType(SyncedDataMixin, OwnerMixin, ObjectType):
site_groups: list[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ConfigTemplate,
fields='__all__',
filters=ConfigTemplateFilter,
@ -106,7 +105,7 @@ class ConfigTemplateType(SyncedDataMixin, OwnerMixin, TagsMixin, ObjectType):
device_roles: list[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.CustomField,
fields='__all__',
filters=CustomFieldFilter,
@ -117,7 +116,7 @@ class CustomFieldType(OwnerMixin, ObjectType):
choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.CustomFieldChoiceSet,
exclude=['extra_choices', 'choice_colors'],
filters=CustomFieldChoiceSetFilter,
@ -130,7 +129,7 @@ class CustomFieldChoiceSetType(OwnerMixin, ObjectType):
choice_colors: JSON
@strawberry_django.type(
@register_type(
models.CustomLink,
fields='__all__',
filters=CustomLinkFilter,
@ -140,7 +139,7 @@ class CustomLinkType(OwnerMixin, ObjectType):
pass
@strawberry_django.type(
@register_type(
models.ExportTemplate,
fields='__all__',
filters=ExportTemplateFilter,
@ -150,7 +149,7 @@ class ExportTemplateType(SyncedDataMixin, OwnerMixin, ObjectType):
pass
@strawberry_django.type(
@register_type(
models.ImageAttachment,
fields='__all__',
filters=ImageAttachmentFilter,
@ -160,7 +159,7 @@ class ImageAttachmentType(BaseObjectType):
object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.JournalEntry,
fields='__all__',
filters=JournalEntryFilter,
@ -171,7 +170,7 @@ class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
created_by: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.Notification,
filters=NotificationFilter,
pagination=True
@ -180,7 +179,7 @@ class NotificationType(ObjectType):
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.NotificationGroup,
filters=NotificationGroupFilter,
pagination=True
@ -190,7 +189,7 @@ class NotificationGroupType(ObjectType):
groups: list[Annotated["GroupType", strawberry.lazy('users.graphql.types')]]
@strawberry_django.type(
@register_type(
models.SavedFilter,
exclude=['content_types',],
filters=SavedFilterFilter,
@ -200,7 +199,7 @@ class SavedFilterType(SharedObjectMixin, OwnerMixin, ObjectType):
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.Subscription,
filters=SubscriptionFilter,
pagination=True
@ -209,7 +208,7 @@ class SubscriptionType(ObjectType):
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.TableConfig,
fields='__all__',
filters=TableConfigFilter,
@ -220,7 +219,7 @@ class TableConfigType(SharedObjectMixin, ObjectType):
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.Tag,
exclude=['extras_taggeditem_items', ],
filters=TagFilter,
@ -232,7 +231,7 @@ class TagType(OwnerMixin, ObjectType):
object_types: list[ContentTypeType]
@strawberry_django.type(
@register_type(
models.Webhook,
exclude=['content_types',],
filters=WebhookFilter,
@ -242,7 +241,7 @@ class WebhookType(OwnerMixin, CustomFieldsMixin, TagsMixin, ObjectType):
pass
@strawberry_django.type(
@register_type(
models.EventRule,
exclude=['content_types',],
filters=EventRuleFilter,

View File

@ -17,6 +17,7 @@ from netbox.graphql.filters import (
NetBoxModelFilter,
OrganizationalModelFilter,
PrimaryModelFilter,
register_filter,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface
@ -52,7 +53,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.ASN, lookups=True)
@register_filter(models.ASN, lookups=True)
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
rir_id: ID | None = strawberry_django.filter_field()
@ -69,7 +70,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
) = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ASNRange, lookups=True)
@register_filter(models.ASNRange, lookups=True)
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -83,7 +84,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
)
@strawberry_django.filter_type(models.Aggregate, lookups=True)
@register_filter(models.Aggregate, lookups=True)
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
prefix: StrFilterLookup | None = strawberry_django.filter_field()
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@ -116,7 +117,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
return Q(**{f"{prefix}prefix__family": value.value})
@strawberry_django.filter_type(models.FHRPGroup, lookups=True)
@register_filter(models.FHRPGroup, lookups=True)
class FHRPGroupFilter(PrimaryModelFilter):
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
@ -134,7 +135,7 @@ class FHRPGroupFilter(PrimaryModelFilter):
)
@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
@register_filter(models.FHRPGroupAssignment, lookups=True)
class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -173,7 +174,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
return Q(**{f"{prefix}interface_id__in": interface_ids})
@strawberry_django.filter_type(models.IPAddress, lookups=True)
@register_filter(models.IPAddress, lookups=True)
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
address: StrFilterLookup | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@ -224,7 +225,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
return Q(**{f"{prefix}address__family": value.value})
@strawberry_django.filter_type(models.IPRange, lookups=True)
@register_filter(models.IPRange, lookups=True)
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
start_address: StrFilterLookup | None = strawberry_django.filter_field()
end_address: StrFilterLookup | None = strawberry_django.filter_field()
@ -278,7 +279,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
return q
@strawberry_django.filter_type(models.Prefix, lookups=True)
@register_filter(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
prefix: StrFilterLookup | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@ -315,19 +316,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
return Q(**{f"{prefix}prefix__family": value.value})
@strawberry_django.filter_type(models.RIR, lookups=True)
@register_filter(models.RIR, lookups=True)
class RIRFilter(OrganizationalModelFilter):
is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Role, lookups=True)
@register_filter(models.Role, lookups=True)
class RoleFilter(OrganizationalModelFilter):
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.RouteTarget, lookups=True)
@register_filter(models.RouteTarget, lookups=True)
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
@ -344,7 +345,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.Service, lookups=True)
@register_filter(models.Service, lookups=True)
class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
@ -356,12 +357,12 @@ class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
parent_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
@register_filter(models.ServiceTemplate, lookups=True)
class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLAN, lookups=True)
@register_filter(models.VLAN, lookups=True)
class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@ -393,7 +394,7 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.VLANGroup, lookups=True)
@register_filter(models.VLANGroup, lookups=True)
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
vid_ranges: Annotated['IntegerRangeArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
@ -401,12 +402,12 @@ class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
total_vlan_ids: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
@register_filter(models.VLANTranslationPolicy, lookups=True)
class VLANTranslationPolicyFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
@register_filter(models.VLANTranslationRule, lookups=True)
class VLANTranslationRuleFilter(NetBoxModelFilter):
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -421,7 +422,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
)
@strawberry_django.filter_type(models.VRF, lookups=True)
@register_filter(models.VRF, lookups=True)
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
rd: StrFilterLookup | None = strawberry_django.filter_field()

View File

@ -8,7 +8,13 @@ from dcim.graphql.types import SiteType
from extras.graphql.mixins import ContactsMixin
from ipam import models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType
from netbox.graphql.types import (
BaseObjectType,
NetBoxObjectType,
OrganizationalObjectType,
PrimaryObjectType,
register_type,
)
from .filters import *
from .mixins import IPAddressesMixin
@ -68,7 +74,7 @@ class BaseIPAddressFamilyType:
return IPAddressFamilyType(value=self.family, label=f'IPv{self.family}')
@strawberry_django.type(
@register_type(
models.ASN,
fields='__all__',
filters=ASNFilter,
@ -84,7 +90,7 @@ class ASNType(ContactsMixin, PrimaryObjectType):
providers: list[ProviderType]
@strawberry_django.type(
@register_type(
models.ASNRange,
fields='__all__',
filters=ASNRangeFilter,
@ -97,7 +103,7 @@ class ASNRangeType(OrganizationalObjectType):
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.Aggregate,
fields='__all__',
filters=AggregateFilter,
@ -109,7 +115,7 @@ class AggregateType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.FHRPGroup,
fields='__all__',
filters=FHRPGroupFilter,
@ -119,7 +125,7 @@ class FHRPGroupType(IPAddressesMixin, PrimaryObjectType):
fhrpgroupassignment_set: list[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.type(
@register_type(
models.FHRPGroupAssignment,
exclude=['interface_type', 'interface_id'],
filters=FHRPGroupAssignmentFilter,
@ -137,7 +143,7 @@ class FHRPGroupAssignmentType(BaseObjectType):
return self.interface
@strawberry_django.type(
@register_type(
models.IPAddress,
exclude=['assigned_object_type', 'assigned_object_id', 'address'],
filters=IPAddressFilter,
@ -163,7 +169,7 @@ class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
return self.assigned_object
@strawberry_django.type(
@register_type(
models.IPRange,
fields='__all__',
filters=IPRangeFilter,
@ -177,7 +183,7 @@ class IPRangeType(ContactsMixin, PrimaryObjectType):
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.Prefix,
exclude=['scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'],
filters=PrefixFilter,
@ -201,7 +207,7 @@ class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
return self.scope
@strawberry_django.type(
@register_type(
models.RIR,
fields='__all__',
filters=RIRFilter,
@ -214,7 +220,7 @@ class RIRType(OrganizationalObjectType):
aggregates: list[Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Role,
fields='__all__',
filters=RoleFilter,
@ -227,7 +233,7 @@ class RoleType(OrganizationalObjectType):
vlans: list[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.type(
@register_type(
models.RouteTarget,
fields='__all__',
filters=RouteTargetFilter,
@ -242,7 +248,7 @@ class RouteTargetType(PrimaryObjectType):
exporting_vrfs: list[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.type(
@register_type(
models.Service,
exclude=('_ports_lowest', 'parent_object_type', 'parent_object_id'),
filters=ServiceFilter,
@ -262,7 +268,7 @@ class ServiceType(ContactsMixin, PrimaryObjectType):
return self.parent
@strawberry_django.type(
@register_type(
models.ServiceTemplate,
exclude=('_ports_lowest',),
filters=ServiceTemplateFilter,
@ -272,7 +278,7 @@ class ServiceTemplateType(PrimaryObjectType):
ports: list[int]
@strawberry_django.type(
@register_type(
models.VLAN,
exclude=['qinq_svlan'],
filters=VLANFilter,
@ -296,7 +302,7 @@ class VLANType(PrimaryObjectType):
return self.qinq_svlan
@strawberry_django.type(
@register_type(
models.VLANGroup,
exclude=['scope_type', 'scope_id'],
filters=VLANGroupFilter,
@ -323,7 +329,7 @@ class VLANGroupType(OrganizationalObjectType):
return self.scope
@strawberry_django.type(
@register_type(
models.VLANTranslationPolicy,
fields='__all__',
filters=VLANTranslationPolicyFilter,
@ -333,7 +339,7 @@ class VLANTranslationPolicyType(PrimaryObjectType):
rules: list[Annotated["VLANTranslationRuleType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.type(
@register_type(
models.VLANTranslationRule,
fields='__all__',
filters=VLANTranslationRuleFilter,
@ -346,7 +352,7 @@ class VLANTranslationRuleType(NetBoxObjectType):
] = strawberry_django.field(select_related=["policy"])
@strawberry_django.type(
@register_type(
models.VRF,
fields='__all__',
filters=VRFFilter,

View File

@ -7,6 +7,7 @@ from strawberry_django import ComparisonFilterLookup, StrFilterLookup
from core.graphql.filter_mixins import ChangeLoggingMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
from netbox.graphql.utils import register_model_graphql_type
if TYPE_CHECKING:
from .filters import *
@ -18,9 +19,20 @@ __all__ = (
'NetBoxModelFilter',
'OrganizationalModelFilter',
'PrimaryModelFilter',
'register_filter',
)
def register_filter(model, **kwargs):
"""
Drop-in replacement for `strawberry_django.filter_type()` for model-bound NetBox GraphQL filters. Before
delegating to `strawberry_django.filter_type()`, any plugin-registered filter mixins for the given model are
spliced into the decorated class's bases. With no extensions registered this is an exact pass-through, leaving
schema output unchanged. See `register_model_graphql_type` for the registry-timing contract.
"""
return register_model_graphql_type(model, strawberry_django.filter_type, 'graphql_filter_extensions', **kwargs)
@dataclass
class BaseModelFilter:
id: ComparisonFilterLookup[ID] | None = strawberry_django.filter_field()

View File

@ -7,6 +7,7 @@ from strawberry.types import Info
from core.graphql.mixins import ChangelogMixin
from core.models import ObjectType as ObjectType_
from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMixin
from netbox.graphql.utils import register_model_graphql_type
from users.graphql.mixins import OwnerMixin
__all__ = (
@ -19,9 +20,20 @@ __all__ = (
'ObjectType',
'OrganizationalObjectType',
'PrimaryObjectType',
'register_type',
)
def register_type(model, **kwargs):
"""
Drop-in replacement for `strawberry_django.type()` for model-bound NetBox GraphQL output types. Before delegating
to `strawberry_django.type()`, any plugin-registered output-type mixins for the given model are spliced into the
decorated class's bases. With no extensions registered this is an exact pass-through, leaving schema output
unchanged. See `register_model_graphql_type` for the registry-timing contract.
"""
return register_model_graphql_type(model, strawberry_django.type, 'graphql_type_extensions', **kwargs)
#
# Base types
#
@ -159,7 +171,7 @@ class NetBoxObjectType(
# Miscellaneous types
#
@strawberry_django.type(
@register_type(
ContentType,
fields=['id', 'app_label', 'model'],
pagination=True
@ -168,7 +180,7 @@ class ContentTypeType:
pass
@strawberry_django.type(
@register_type(
ObjectType_,
fields=['id', 'app_label', 'model'],
pagination=True

View File

@ -0,0 +1,126 @@
import logging
from netbox.registry import registry
__all__ = (
'get_model_label',
'register_model_graphql_type',
'splice_extension_bases',
)
def get_model_label(model):
"""
Return the canonical `app_label.model_name` label used to key GraphQL extensions in the registry. Both the
registration side and the lookup side must derive labels through this helper so they always agree.
"""
return f'{model._meta.app_label}.{model._meta.model_name}'
def _own_names(klass):
"""
Return the set of field/attribute names a single class contributes directly: its annotations and its own
non-dunder attributes (such as resolver methods), excluding the `models` extension marker.
"""
names = set(getattr(klass, '__annotations__', {}))
names |= {name for name in vars(klass) if not name.startswith('__')}
names.discard('models')
return names
def _core_names(cls):
"""
Return every name `cls` resolves (its own body and everything it inherits). Extensions are spliced in *after*
these bases, so any name already present here is provided by the core type and an extension cannot override it.
"""
names = set()
for klass in cls.__mro__:
if klass is object:
continue
names |= _own_names(klass)
return names
def splice_extension_bases(cls, extensions):
"""
Return a class equivalent to `cls` but with the given plugin extension mixin classes spliced into its bases,
so that fields/filters they declare are picked up when the class is processed by Strawberry.
If `extensions` is empty, `cls` is returned unchanged (an exact pass-through). Otherwise a new class is built
with the same name and namespace as `cls` preserving its own annotations, fields, and methods with the
extension classes appended to its bases.
Precedence: extensions are appended *after* `cls`'s base classes in the MRO, so extensions are strictly
additive. Any name the core type already provides (its own fields or anything it inherits, including hooks such
as `get_queryset`) always wins; an extension declaring such a name is ignored. When two extensions declare the
same new name, the one whose plugin loaded first wins (it is registered earlier). Both cases are warned about.
"""
if not extensions:
return cls
# Fetch the logger lazily rather than at module import. Importing this module during settings/app loading
# (e.g. via the plugin registration helpers) happens before Django configures logging; creating the logger
# then would get it disabled by a `disable_existing_loggers` LOGGING config.
logger = logging.getLogger('netbox.graphql')
# Warn on field-name collisions so they can be diagnosed in deployments with many plugins.
core_names = _core_names(cls)
seen = {}
for extension in extensions:
for name in _own_names(extension):
if name in core_names:
logger.warning(
"GraphQL extension %s declares '%s', which core type %s already provides; the extension's "
"version is ignored (core takes precedence).",
extension, name, cls.__name__,
)
elif name in seen:
logger.warning(
"GraphQL extensions %s and %s both define '%s' on %s; %s takes precedence because its "
"plugin is loaded first.",
seen[name], extension, name, cls.__name__, seen[name],
)
else:
seen[name] = extension
namespace = dict(cls.__dict__)
# Drop the descriptors that cannot (and need not) be copied to the rebuilt class; they are recreated by the
# metaclass call below.
namespace.pop('__dict__', None)
namespace.pop('__weakref__', None)
bases = (*cls.__bases__, *extensions)
# Rebuild via the class's own metaclass rather than the built-in `type`, so a core type using a custom
# metaclass is preserved. Pre-decoration Strawberry/dataclass types use the plain `type` metaclass.
try:
return type(cls)(cls.__name__, bases, namespace)
except TypeError as exc:
raise TypeError(
f"Failed to splice GraphQL extension(s) {[e.__name__ for e in extensions]} into core type "
f"'{cls.__name__}': {exc}. A GraphQL extension should be a plain @strawberry.type mixin that only "
f"adds fields; inheriting from classes already in the core type's base list can produce an "
f"inconsistent MRO."
) from exc
def register_model_graphql_type(model, delegate, store_key, **kwargs):
"""
Shared implementation behind `register_type` and `register_filter`. Returns a decorator that splices any
plugin extensions for `model` (from `store_key`) into the decorated class, then delegates to `delegate`
(`strawberry_django.type` / `filter_type`).
The registry is read at decoration (import) time. This is safe because the schema is assembled lazily from the
URLconf, after every plugin's `ready()` has run; importing a core `graphql/types.py` during app init would read
the registry too early and silently drop later-registered extensions.
"""
label = get_model_label(model)
def wrapper(cls):
# Record that this type/filter has been assembled, so a plugin that registers an extension after this
# point (e.g. because its ready() imported a core graphql module early) can be warned it is too late.
registry['plugins']['graphql_extensions_assembled'].add((store_key, label))
extensions = registry['plugins'][store_key].get(label)
cls = splice_extension_bases(cls, extensions)
return delegate(model, **kwargs)(cls)
return wrapper

View File

@ -21,6 +21,11 @@ registry['plugins'].update({
'installed': [],
'graphql_schemas': [],
'jinja_filters': {},
'graphql_type_extensions': collections.defaultdict(list),
'graphql_filter_extensions': collections.defaultdict(list),
# (store_key, label) pairs whose core type/filter has already been assembled, used to detect extensions
# registered too late to be spliced in.
'graphql_extensions_assembled': set(),
'menus': [],
'menu_items': {},
'preferences': {},
@ -32,6 +37,8 @@ DEFAULT_RESOURCE_PATHS = {
'data_backends': 'data_backends.backends',
'graphql_schema': 'graphql.schema',
'jinja_filters': 'jinja_env.filters',
'graphql_type_extensions': 'graphql.type_extensions',
'graphql_filter_extensions': 'graphql.filter_extensions',
'menu': 'navigation.menu',
'menu_items': 'navigation.menu_items',
'template_extensions': 'template_content.template_extensions',
@ -81,6 +88,8 @@ class PluginConfig(AppConfig):
data_backends = None
graphql_schema = None
jinja_filters = None
graphql_type_extensions = None
graphql_filter_extensions = None
menu = None
menu_items = None
serializer_resolver = None
@ -151,6 +160,13 @@ class PluginConfig(AppConfig):
if graphql_schema := self._load_resource('graphql_schema'):
register_graphql_schema(graphql_schema)
# Register GraphQL type & filter extensions (if defined). These must be registered before the GraphQL
# schema is assembled (during ROOT_URLCONF loading), which occurs after all apps' ready() methods run.
if graphql_type_extensions := self._load_resource('graphql_type_extensions'):
register_graphql_type_extensions(graphql_type_extensions)
if graphql_filter_extensions := self._load_resource('graphql_filter_extensions'):
register_graphql_filter_extensions(graphql_filter_extensions)
# Register user preferences (if defined)
if user_preferences := self._load_resource('user_preferences'):
register_user_preferences(plugin_name, user_preferences)

View File

@ -1,8 +1,10 @@
import inspect
import logging
from django.apps import apps
from django.utils.translation import gettext_lazy as _
from netbox.graphql.utils import get_model_label
from netbox.registry import registry
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
@ -11,7 +13,9 @@ from .templates import PluginTemplateExtension
logger = logging.getLogger(__name__)
__all__ = (
'register_graphql_filter_extensions',
'register_graphql_schema',
'register_graphql_type_extensions',
'register_jinja_filters',
'register_menu',
'register_menu_items',
@ -102,6 +106,74 @@ def register_graphql_schema(graphql_schema):
registry['plugins']['graphql_schemas'].extend(graphql_schema)
def _register_graphql_extensions(class_list, store):
"""
Collect a list of GraphQL output-type or filter mixin classes into the given registry store, bucketed by the
model labels declared on each class's `models` attribute. Each declared label is validated against the app
registry and normalized to the canonical `app_label.model_name` form (via `get_model_label`) so that the
stored key always matches the label `register_type`/`register_filter` look up.
"""
for extension in class_list:
if not inspect.isclass(extension):
raise TypeError(
_("GraphQL extension {extension} was passed as an instance!").format(extension=extension)
)
models = getattr(extension, 'models', None)
if not models:
raise TypeError(
_("GraphQL extension {extension} must declare a non-empty 'models' attribute.").format(
extension=extension
)
)
# Must be @strawberry.type-decorated for its fields to be collected. Check the class's own __dict__ (not
# hasattr) so an undecorated subclass of a @strawberry.type base is still rejected. `__strawberry_definition__`
# is a Strawberry internal (verified against strawberry-graphql 0.321.0); revisit on dependency upgrades.
if '__strawberry_definition__' not in vars(extension):
raise TypeError(
_("GraphQL extension {extension} must be decorated with @strawberry.type.").format(
extension=extension
)
)
for label in models:
# Resolve the model to validate the label and derive its canonical key; a bad label would otherwise
# register into a bucket that is never looked up, silently dropping the extension.
try:
model = apps.get_model(label)
except (LookupError, ValueError):
raise TypeError(
_("GraphQL extension {extension} targets unknown model '{label}'.").format(
extension=extension, label=label
)
)
canonical_label = get_model_label(model)
# If the core type/filter was already assembled (a plugin imported a core graphql module during
# ready()), this extension is too late to be spliced in and will not appear in the schema. Fetch the
# logger lazily (not the module-level one) so it isn't disabled by a `disable_existing_loggers` config.
if (store, canonical_label) in registry['plugins']['graphql_extensions_assembled']:
logging.getLogger('netbox.graphql').warning(
"GraphQL extension %s for '%s' was registered after the core type was assembled and will be "
"ignored. Avoid importing core GraphQL modules from a plugin's ready().",
extension, canonical_label,
)
registry['plugins'][store][canonical_label].append(extension)
def register_graphql_type_extensions(class_list):
"""
Register a list of GraphQL output-type mixin classes. Each class must be decorated with @strawberry.type and
declare a `models` attribute listing the `app_label.model` labels of the core types it extends.
"""
_register_graphql_extensions(class_list, 'graphql_type_extensions')
def register_graphql_filter_extensions(class_list):
"""
Register a list of GraphQL filter mixin classes. Each class must be decorated with @strawberry.type and declare
a `models` attribute listing the `app_label.model` labels of the core filters it extends.
"""
_register_graphql_extensions(class_list, 'graphql_filter_extensions')
def register_user_preferences(plugin_name, preferences):
"""
Register a list of user preferences defined by a plugin.

View File

@ -1,6 +1,7 @@
import strawberry
import strawberry_django
from django.db.models import Q
from . import models
@ -22,3 +23,35 @@ class DummyQuery:
schema = [
DummyQuery,
]
#
# Extensions to core GraphQL types & filters (see netbox.graphql.types.register_type /
# netbox.graphql.filters.register_filter). These exercise the plugin extension point.
#
@strawberry.type
class SiteTypeExtension:
models = ['dcim.site']
@strawberry_django.field
def dummy_plugin_field(self) -> str:
return 'dummy-plugin-value'
@strawberry.type
class SiteFilterExtension:
models = ['dcim.site']
@strawberry_django.filter_field()
def dummy_plugin_filter(self, value: str, prefix) -> Q:
return Q(**{f'{prefix}name': value})
type_extensions = [
SiteTypeExtension,
]
filter_extensions = [
SiteFilterExtension,
]

View File

@ -1,7 +1,9 @@
import json
import re
from unittest import skipIf
import strawberry
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.test import override_settings
@ -133,6 +135,26 @@ class GraphQLAPITestCase(APITestCase):
)
Site.objects.bulk_create(sites)
@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
@override_settings(LOGIN_REQUIRED=True)
def test_graphql_plugin_extensions_execute(self):
"""
A plugin-provided filter extension and field extension execute end-to-end against a live query,
exercising the custom filter method's prefix plumbing and the type extension's resolver.
"""
self.add_permissions('dcim.view_site')
url = reverse('graphql')
query = '{ site_list(filters: {dummy_plugin_filter: "Site 1"}) { name dummy_plugin_field } }'
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)
sites = data['data']['site_list']
self.assertEqual(len(sites), 1)
self.assertEqual(sites[0]['name'], 'Site 1')
self.assertEqual(sites[0]['dummy_plugin_field'], 'dummy-plugin-value')
@override_settings(LOGIN_REQUIRED=True)
def test_graphql_filter_objects(self):
"""
@ -662,3 +684,130 @@ class JSONStringLookupTestCase(TestCase):
'starts_with', 'i_starts_with', 'ends_with', 'i_ends_with',
'in_', 'isnull', 'regex', 'i_regex'):
self.assertIn(expected, field_names, f"{expected!r} must be present on JSONStringLookup")
class SpliceExtensionBasesTestCase(TestCase):
"""Verify splice_extension_bases() behavior: pass-through, splicing, and collision warnings."""
@staticmethod
def _make_core():
@strawberry.type
class CoreBase:
description: str # inherited (non-protected) field
@classmethod
def get_queryset(cls, queryset, info, **kwargs):
return queryset
@strawberry.type
class CoreType(CoreBase):
name: str # defined directly in the core type's own body
return CoreType
def test_no_extensions_is_passthrough(self):
from netbox.graphql.utils import splice_extension_bases
CoreType = self._make_core()
self.assertIs(splice_extension_bases(CoreType, []), CoreType)
self.assertIs(splice_extension_bases(CoreType, None), CoreType)
def test_extension_spliced_into_bases(self):
from netbox.graphql.utils import splice_extension_bases
@strawberry.type
class Extension:
models = ['dcim.device']
extra: str
CoreType = self._make_core()
result = splice_extension_bases(CoreType, [Extension])
self.assertIsNot(result, CoreType)
self.assertEqual(result.__name__, CoreType.__name__)
self.assertIn(Extension, result.__mro__)
# The extension is appended *after* the core bases in the MRO (additive, core wins collisions)
self.assertGreater(result.__mro__.index(Extension), result.__mro__.index(CoreType.__bases__[0]))
def test_warns_when_extension_collides_with_core_own_field(self):
# A name the core type defines directly always wins; the extension's version is ignored (and warned).
from netbox.graphql.utils import splice_extension_bases
@strawberry.type
class Extension:
models = ['dcim.device']
name: str # collides with CoreType.name (own body)
CoreType = self._make_core()
with self.assertLogs('netbox.graphql', level='WARNING') as cm:
splice_extension_bases(CoreType, [Extension])
self.assertTrue(any("already provides" in msg and "core takes precedence" in msg for msg in cm.output))
def test_warns_when_extension_collides_with_inherited_field(self):
# A name the core type inherits also wins over the extension (extensions are strictly additive).
from netbox.graphql.utils import splice_extension_bases
@strawberry.type
class Extension:
models = ['dcim.device']
description: str # collides with CoreBase.description (inherited)
CoreType = self._make_core()
with self.assertLogs('netbox.graphql', level='WARNING') as cm:
splice_extension_bases(CoreType, [Extension])
self.assertTrue(any("already provides" in msg for msg in cm.output))
def test_core_hook_wins_over_extension(self):
# An extension declaring get_queryset is ignored; the core permission-enforcing hook is preserved by
# ordering (extensions are appended after the core bases).
from netbox.graphql.utils import splice_extension_bases
@strawberry.type
class Extension:
models = ['dcim.device']
@classmethod
def get_queryset(cls, queryset, info, **kwargs):
return 'EXTENSION_WON'
CoreType = self._make_core()
with self.assertLogs('netbox.graphql', level='WARNING') as cm:
result = splice_extension_bases(CoreType, [Extension])
self.assertTrue(any("already provides" in msg and "get_queryset" in msg for msg in cm.output))
# Core's get_queryset (identity) is retained, not the extension's override
self.assertEqual(result.get_queryset('CORE_QS', None), 'CORE_QS')
def test_mro_conflict_raises_clear_error(self):
from netbox.graphql.utils import splice_extension_bases
class A:
pass
class B:
pass
class Core(A, B):
name = 'core'
class Extension(B, A): # reversed base order -> inconsistent MRO when spliced
models = ['dcim.device']
with self.assertRaises(TypeError) as ctx:
splice_extension_bases(Core, [Extension])
self.assertIn('Failed to splice', str(ctx.exception))
def test_warns_on_collision_between_extensions(self):
from netbox.graphql.utils import splice_extension_bases
@strawberry.type
class ExtensionA:
models = ['dcim.device']
widgets: str
@strawberry.type
class ExtensionB:
models = ['dcim.device']
widgets: str
CoreType = self._make_core()
with self.assertLogs('netbox.graphql', level='WARNING') as cm:
splice_extension_bases(CoreType, [ExtensionA, ExtensionB])
self.assertTrue(any("both define" in msg and "loaded first" in msg for msg in cm.output))

View File

@ -1,3 +1,4 @@
import re
from unittest import skipIf
from django.conf import settings
@ -216,6 +217,26 @@ class PluginTestCase(TestCase):
self.assertIn(DummyQuery, registry['plugins']['graphql_schemas'])
self.assertTrue(issubclass(Query, DummyQuery))
def test_graphql_type_extensions(self):
"""
Validate that plugin GraphQL type & filter extensions are registered and spliced into the built schema.
"""
from netbox.graphql.schema import schema
from netbox.tests.dummy_plugin.graphql import SiteFilterExtension, SiteTypeExtension
# Extensions are registered against the targeted core model
self.assertIn(SiteTypeExtension, registry['plugins']['graphql_type_extensions']['dcim.site'])
self.assertIn(SiteFilterExtension, registry['plugins']['graphql_filter_extensions']['dcim.site'])
# The injected field and filter appear in the assembled schema
schema_str = schema.as_str()
site_type = re.search(r'\ntype SiteType \{.*?\n\}', schema_str, re.DOTALL)
self.assertIsNotNone(site_type, "SiteType not found in GraphQL schema")
self.assertIn('dummy_plugin_field', site_type.group(0))
site_filter = re.search(r'\ninput SiteFilter \{.*?\n\}', schema_str, re.DOTALL)
self.assertIsNotNone(site_filter, "SiteFilter not found in GraphQL schema")
self.assertIn('dummy_plugin_filter', site_filter.group(0))
@override_settings(PLUGINS_CONFIG={'netbox.tests.dummy_plugin': {'foo': 123}})
def test_get_plugin_config(self):
"""
@ -359,3 +380,93 @@ class PluginNavigationTestCase(TestCase):
self.assertIsNot(item1.permissions, item2.permissions)
self.assertEqual(item1.permissions, ['explicit_permission'])
self.assertEqual(item2.permissions, ['different_permission'])
class RegisterGraphQLExtensionsTestCase(TestCase):
"""Validate registration-time checks for GraphQL type/filter extensions."""
def test_rejects_extension_without_models(self):
import strawberry
from netbox.plugins.registration import register_graphql_type_extensions
@strawberry.type
class NoModels:
pass
with self.assertRaises(TypeError):
register_graphql_type_extensions([NoModels])
def test_rejects_undecorated_extension(self):
# A plain class (no @strawberry.type) must be rejected...
from netbox.plugins.registration import register_graphql_type_extensions
class Undecorated:
models = ['dcim.device']
with self.assertRaises(TypeError):
register_graphql_type_extensions([Undecorated])
def test_rejects_undecorated_subclass_of_strawberry_type(self):
# ...as must a subclass that only inherits __strawberry_definition__ without its own decoration.
import strawberry
from netbox.plugins.registration import register_graphql_type_extensions
@strawberry.type
class Base:
pass
class Child(Base):
models = ['dcim.device']
with self.assertRaises(TypeError):
register_graphql_type_extensions([Child])
def test_rejects_unknown_model_label(self):
import strawberry
from netbox.plugins.registration import register_graphql_type_extensions
@strawberry.type
class BadTarget:
models = ['dcim.notamodel']
with self.assertRaises(TypeError):
register_graphql_type_extensions([BadTarget])
def test_filter_extension_requires_strawberry_type(self):
# The filter path enforces the same @strawberry.type requirement as the type path.
from netbox.plugins.registration import register_graphql_filter_extensions
class UndecoratedFilter:
models = ['dcim.device']
with self.assertRaises(TypeError):
register_graphql_filter_extensions([UndecoratedFilter])
def test_warns_when_registered_after_assembly(self):
# An extension registered after its core type was already assembled is warned and will be dropped.
import strawberry
from netbox.plugins.registration import register_graphql_type_extensions
@strawberry.type
class LateExt:
models = ['dcim.cable']
late_field: str
store, label = 'graphql_type_extensions', 'dcim.cable'
assembled = registry['plugins']['graphql_extensions_assembled']
was_present = (store, label) in assembled
assembled.add((store, label))
# Restore global registry state regardless of outcome so other tests are unaffected.
self.addCleanup(lambda: registry['plugins'][store].__setitem__(
label, [e for e in registry['plugins'][store][label] if e is not LateExt]
))
if not was_present:
self.addCleanup(assembled.discard, (store, label))
with self.assertLogs('netbox.graphql', level='WARNING') as cm:
register_graphql_type_extensions([LateExt])
self.assertTrue(any('after the core type was assembled' in msg for msg in cm.output))

View File

@ -11,6 +11,7 @@ from netbox.graphql.filters import (
NestedGroupModelFilter,
OrganizationalModelFilter,
PrimaryModelFilter,
register_filter,
)
from tenancy import models
@ -58,7 +59,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.Tenant, lookups=True)
@register_filter(models.Tenant, lookups=True)
class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -137,7 +138,7 @@ class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.TenantGroup, lookups=True)
@register_filter(models.TenantGroup, lookups=True)
class TenantGroupFilter(OrganizationalModelFilter):
parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -151,7 +152,7 @@ class TenantGroupFilter(OrganizationalModelFilter):
)
@strawberry_django.filter_type(models.Contact, lookups=True)
@register_filter(models.Contact, lookups=True)
class ContactFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
title: StrFilterLookup | None = strawberry_django.filter_field()
@ -167,19 +168,19 @@ class ContactFilter(PrimaryModelFilter):
)
@strawberry_django.filter_type(models.ContactRole, lookups=True)
@register_filter(models.ContactRole, lookups=True)
class ContactRoleFilter(OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.ContactGroup, lookups=True)
@register_filter(models.ContactGroup, lookups=True)
class ContactGroupFilter(NestedGroupModelFilter):
parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ContactAssignment, lookups=True)
@register_filter(models.ContactAssignment, lookups=True)
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()

View File

@ -1,10 +1,15 @@
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, NestedLtreeGroupObjectType, OrganizationalObjectType, PrimaryObjectType
from netbox.graphql.types import (
BaseObjectType,
NestedLtreeGroupObjectType,
OrganizationalObjectType,
PrimaryObjectType,
register_type,
)
from tenancy import models
from .filters import *
@ -52,7 +57,7 @@ __all__ = (
# Tenants
#
@strawberry_django.type(
@register_type(
models.Tenant,
fields='__all__',
filters=TenantFilter,
@ -86,7 +91,7 @@ class TenantType(ContactsMixin, PrimaryObjectType):
l2vpns: list[Annotated['L2VPNType', strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.TenantGroup,
exclude=['path', 'sort_path'],
filters=TenantGroupFilter,
@ -103,7 +108,7 @@ class TenantGroupType(NestedLtreeGroupObjectType):
# Contacts
#
@strawberry_django.type(
@register_type(
models.Contact,
fields='__all__',
filters=ContactFilter,
@ -113,7 +118,7 @@ class ContactType(ContactAssignmentsMixin, PrimaryObjectType):
groups: list[Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ContactRole,
fields='__all__',
filters=ContactRoleFilter,
@ -123,7 +128,7 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType):
pass
@strawberry_django.type(
@register_type(
models.ContactGroup,
exclude=['path', 'sort_path'],
filters=ContactGroupFilter,
@ -136,7 +141,7 @@ class ContactGroupType(NestedLtreeGroupObjectType):
children: list[Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ContactAssignment,
fields='__all__',
filters=ContactAssignmentFilter,

View File

@ -4,7 +4,7 @@ import strawberry
import strawberry_django
from strawberry_django import DatetimeFilterLookup, FilterLookup, StrFilterLookup
from netbox.graphql.filters import BaseModelFilter
from netbox.graphql.filters import BaseModelFilter, register_filter
from users import models
__all__ = (
@ -15,13 +15,13 @@ __all__ = (
)
@strawberry_django.filter_type(models.Group, lookups=True)
@register_filter(models.Group, lookups=True)
class GroupFilter(BaseModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.User, lookups=True)
@register_filter(models.User, lookups=True)
class UserFilter(BaseModelFilter):
username: StrFilterLookup | None = strawberry_django.filter_field()
first_name: StrFilterLookup | None = strawberry_django.filter_field()
@ -34,7 +34,7 @@ class UserFilter(BaseModelFilter):
groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Owner, lookups=True)
@register_filter(models.Owner, lookups=True)
class OwnerFilter(BaseModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()
@ -47,7 +47,7 @@ class OwnerFilter(BaseModelFilter):
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.OwnerGroup, lookups=True)
@register_filter(models.OwnerGroup, lookups=True)
class OwnerGroupFilter(BaseModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
description: StrFilterLookup | None = strawberry_django.filter_field()

View File

@ -1,7 +1,6 @@
import strawberry_django
from netbox.graphql.types import BaseObjectType
from netbox.graphql.types import BaseObjectType, register_type
from users.models import Group, Owner, OwnerGroup, User
from .filters import *
@ -14,7 +13,7 @@ __all__ = (
)
@strawberry_django.type(
@register_type(
Group,
fields=['id', 'name'],
filters=GroupFilter,
@ -24,7 +23,7 @@ class GroupType(BaseObjectType):
pass
@strawberry_django.type(
@register_type(
User,
fields=[
'id', 'username', 'first_name', 'last_name', 'email', 'is_active', 'date_joined', 'groups',
@ -36,7 +35,7 @@ class UserType(BaseObjectType):
groups: list[GroupType]
@strawberry_django.type(
@register_type(
OwnerGroup,
fields=['id', 'name', 'description'],
filters=OwnerGroupFilter,
@ -46,7 +45,7 @@ class OwnerGroupType(BaseObjectType):
pass
@strawberry_django.type(
@register_type(
Owner,
fields=['id', 'group', 'name', 'description', 'user_groups', 'users'],
filters=OwnerFilter,

View File

@ -8,7 +8,7 @@ from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLo
from dcim.graphql.filter_mixins import InterfaceBaseFilterMixin, RenderConfigFilterMixin, ScopedFilterMixin
from extras.graphql.filter_mixins import ConfigContextFilterMixin
from netbox.graphql.filter_mixins import ImageAttachmentFilterMixin
from netbox.graphql.filters import NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter
from netbox.graphql.filters import NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter, register_filter
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization import models
from virtualization.graphql.filter_mixins import VMComponentFilterMixin
@ -38,7 +38,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.Cluster, lookups=True)
@register_filter(models.Cluster, lookups=True)
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
@ -57,19 +57,19 @@ class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, P
)
@strawberry_django.filter_type(models.ClusterGroup, lookups=True)
@register_filter(models.ClusterGroup, lookups=True)
class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilter):
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ClusterType, lookups=True)
@register_filter(models.ClusterType, lookups=True)
class ClusterTypeFilter(OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.VirtualMachineType, lookups=True)
@register_filter(models.VirtualMachineType, lookups=True)
class VirtualMachineTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilter):
default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -87,7 +87,7 @@ class VirtualMachineTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilter):
virtual_machine_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualMachine, lookups=True)
@register_filter(models.VirtualMachine, lookups=True)
class VirtualMachineFilter(
ContactFilterMixin,
ImageAttachmentFilterMixin,
@ -157,7 +157,7 @@ class VirtualMachineFilter(
)
@strawberry_django.filter_type(models.VMInterface, lookups=True)
@register_filter(models.VMInterface, lookups=True)
class VMInterfaceFilter(InterfaceBaseFilterMixin, VMComponentFilterMixin, NetBoxModelFilter):
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -182,7 +182,7 @@ class VMInterfaceFilter(InterfaceBaseFilterMixin, VMComponentFilterMixin, NetBox
)
@strawberry_django.filter_type(models.VirtualDisk, lookups=True)
@register_filter(models.VirtualDisk, lookups=True)
class VirtualDiskFilter(VMComponentFilterMixin, NetBoxModelFilter):
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()

View File

@ -6,7 +6,7 @@ import strawberry_django
from extras.graphql.mixins import ConfigContextMixin, ContactsMixin
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType
from netbox.graphql.types import NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType, register_type
from users.graphql.mixins import OwnerMixin
from virtualization import models
@ -46,7 +46,7 @@ class ComponentType(OwnerMixin, NetBoxObjectType):
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]
@strawberry_django.type(
@register_type(
models.Cluster,
exclude=['scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'],
filters=ClusterFilter,
@ -70,7 +70,7 @@ class ClusterType(ContactsMixin, VLANGroupsMixin, PrimaryObjectType):
return self.scope
@strawberry_django.type(
@register_type(
models.ClusterGroup,
fields='__all__',
filters=ClusterGroupFilter,
@ -81,7 +81,7 @@ class ClusterGroupType(ContactsMixin, VLANGroupsMixin, OrganizationalObjectType)
clusters: list[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]
@strawberry_django.type(
@register_type(
models.ClusterType,
fields='__all__',
filters=ClusterTypeFilter,
@ -92,7 +92,7 @@ class ClusterTypeType(OrganizationalObjectType):
clusters: list[ClusterType]
@strawberry_django.type(
@register_type(
models.VirtualMachineType,
fields='__all__',
filters=VirtualMachineTypeFilter,
@ -105,7 +105,7 @@ class VirtualMachineTypeType(PrimaryObjectType):
instances: list[Annotated['VirtualMachineType', strawberry.lazy('virtualization.graphql.types')]]
@strawberry_django.type(
@register_type(
models.VirtualMachine,
fields='__all__',
filters=VirtualMachineFilter,
@ -131,7 +131,7 @@ class VirtualMachineType(ConfigContextMixin, ContactsMixin, PrimaryObjectType):
virtualdisks: list[Annotated["VirtualDiskType", strawberry.lazy('virtualization.graphql.types')]]
@strawberry_django.type(
@register_type(
models.VMInterface,
fields='__all__',
filters=VMInterfaceFilter,
@ -154,7 +154,7 @@ class VMInterfaceType(IPAddressesMixin, ComponentType):
mac_addresses: list[Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@register_type(
models.VirtualDisk,
fields='__all__',
filters=VirtualDiskFilter,

View File

@ -11,6 +11,7 @@ from netbox.graphql.filters import (
NetBoxModelFilter,
OrganizationalModelFilter,
PrimaryModelFilter,
register_filter,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from vpn import models
@ -36,12 +37,12 @@ __all__ = (
)
@strawberry_django.filter_type(models.TunnelGroup, lookups=True)
@register_filter(models.TunnelGroup, lookups=True)
class TunnelGroupFilter(OrganizationalModelFilter):
pass
@strawberry_django.filter_type(models.TunnelTermination, lookups=True)
@register_filter(models.TunnelTermination, lookups=True)
class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
tunnel: Annotated['TunnelFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field()
tunnel_id: ID | None = strawberry_django.filter_field()
@ -61,7 +62,7 @@ class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLo
outside_ip_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Tunnel, lookups=True)
@register_filter(models.Tunnel, lookups=True)
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
@ -87,7 +88,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.IKEProposal, lookups=True)
@register_filter(models.IKEProposal, lookups=True)
class IKEProposalFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
authentication_method: (
@ -116,7 +117,7 @@ class IKEProposalFilter(PrimaryModelFilter):
)
@strawberry_django.filter_type(models.IKEPolicy, lookups=True)
@register_filter(models.IKEPolicy, lookups=True)
class IKEPolicyFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
version: BaseFilterLookup[Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
@ -131,7 +132,7 @@ class IKEPolicyFilter(PrimaryModelFilter):
preshared_key: StrFilterLookup | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.IPSecProposal, lookups=True)
@register_filter(models.IPSecProposal, lookups=True)
class IPSecProposalFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
encryption_algorithm: (
@ -157,7 +158,7 @@ class IPSecProposalFilter(PrimaryModelFilter):
)
@strawberry_django.filter_type(models.IPSecPolicy, lookups=True)
@register_filter(models.IPSecPolicy, lookups=True)
class IPSecPolicyFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
@ -168,7 +169,7 @@ class IPSecPolicyFilter(PrimaryModelFilter):
)
@strawberry_django.filter_type(models.IPSecProfile, lookups=True)
@register_filter(models.IPSecProfile, lookups=True)
class IPSecProfileFilter(PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
mode: BaseFilterLookup[Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
@ -184,7 +185,7 @@ class IPSecProfileFilter(PrimaryModelFilter):
ipsec_policy_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.L2VPN, lookups=True)
@register_filter(models.L2VPN, lookups=True)
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
name: StrFilterLookup | None = strawberry_django.filter_field()
slug: StrFilterLookup | None = strawberry_django.filter_field()
@ -208,7 +209,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
)
@strawberry_django.filter_type(models.L2VPNTermination, lookups=True)
@register_filter(models.L2VPNTermination, lookups=True)
class L2VPNTerminationFilter(NetBoxModelFilter):
l2vpn: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field()
l2vpn_id: ID | None = strawberry_django.filter_field()

View File

@ -4,7 +4,13 @@ import strawberry
import strawberry_django
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
from netbox.graphql.types import (
NetBoxObjectType,
ObjectType,
OrganizationalObjectType,
PrimaryObjectType,
register_type,
)
from vpn import models
from .filters import *
@ -30,7 +36,7 @@ __all__ = (
)
@strawberry_django.type(
@register_type(
models.TunnelGroup,
fields='__all__',
filters=TunnelGroupFilter,
@ -41,7 +47,7 @@ class TunnelGroupType(ContactsMixin, OrganizationalObjectType):
tunnels: list[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.TunnelTermination,
fields='__all__',
filters=TunnelTerminationFilter,
@ -53,7 +59,7 @@ class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
outside_ip: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
@strawberry_django.type(
@register_type(
models.Tunnel,
fields='__all__',
filters=TunnelFilter,
@ -67,7 +73,7 @@ class TunnelType(ContactsMixin, PrimaryObjectType):
terminations: list[Annotated["TunnelTerminationType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.IKEProposal,
fields='__all__',
filters=IKEProposalFilter,
@ -77,7 +83,7 @@ class IKEProposalType(PrimaryObjectType):
ike_policies: list[Annotated["IKEPolicyType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.IKEPolicy,
fields='__all__',
filters=IKEPolicyFilter,
@ -88,7 +94,7 @@ class IKEPolicyType(PrimaryObjectType):
ipsec_profiles: list[Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.IPSecProposal,
fields='__all__',
filters=IPSecProposalFilter,
@ -98,7 +104,7 @@ class IPSecProposalType(PrimaryObjectType):
ipsec_policies: list[Annotated["IPSecPolicyType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.IPSecPolicy,
fields='__all__',
filters=IPSecPolicyFilter,
@ -109,7 +115,7 @@ class IPSecPolicyType(PrimaryObjectType):
ipsec_profiles: list[Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.IPSecProfile,
fields='__all__',
filters=IPSecProfileFilter,
@ -122,7 +128,7 @@ class IPSecProfileType(PrimaryObjectType):
tunnels: list[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]]
@strawberry_django.type(
@register_type(
models.L2VPN,
fields='__all__',
filters=L2VPNFilter,
@ -136,7 +142,7 @@ class L2VPNType(ContactsMixin, PrimaryObjectType):
import_targets: list[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.type(
@register_type(
models.L2VPNTermination,
exclude=['assigned_object_type', 'assigned_object_id'],
filters=L2VPNTerminationFilter,

View File

@ -7,7 +7,7 @@ from strawberry_django import BaseFilterLookup, StrFilterLookup
from dcim.graphql.filter_mixins import ScopedFilterMixin
from netbox.graphql.filter_mixins import DistanceFilterMixin
from netbox.graphql.filters import NestedGroupModelFilter, PrimaryModelFilter
from netbox.graphql.filters import NestedGroupModelFilter, PrimaryModelFilter, register_filter
from tenancy.graphql.filter_mixins import TenancyFilterMixin
from wireless import models
@ -26,12 +26,12 @@ __all__ = (
)
@strawberry_django.filter_type(models.WirelessLANGroup, lookups=True)
@register_filter(models.WirelessLANGroup, lookups=True)
class WirelessLANGroupFilter(NestedGroupModelFilter):
pass
@strawberry_django.filter_type(models.WirelessLAN, lookups=True)
@register_filter(models.WirelessLAN, lookups=True)
class WirelessLANFilter(
WirelessAuthenticationFilterMixin,
ScopedFilterMixin,
@ -50,7 +50,7 @@ class WirelessLANFilter(
vlan_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.WirelessLink, lookups=True)
@register_filter(models.WirelessLink, lookups=True)
class WirelessLinkFilter(
WirelessAuthenticationFilterMixin,
DistanceFilterMixin,

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
from netbox.graphql.types import NestedLtreeGroupObjectType, PrimaryObjectType
from netbox.graphql.types import NestedLtreeGroupObjectType, PrimaryObjectType, register_type
from wireless import models
from .filters import *
@ -20,7 +20,7 @@ __all__ = (
)
@strawberry_django.type(
@register_type(
models.WirelessLANGroup,
exclude=['path', 'sort_path'],
filters=WirelessLANGroupFilter,
@ -33,7 +33,7 @@ class WirelessLANGroupType(NestedLtreeGroupObjectType):
children: list[Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]]
@strawberry_django.type(
@register_type(
models.WirelessLAN,
exclude=['scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'],
filters=WirelessLANFilter,
@ -57,7 +57,7 @@ class WirelessLANType(PrimaryObjectType):
return self.scope
@strawberry_django.type(
@register_type(
models.WirelessLink,
fields='__all__',
filters=WirelessLinkFilter,