Skip to content

Commit c35f5f8

Browse files
Closes #7598: Enable custom field filtering for GraphQL (#18701)
1 parent bbf4eea commit c35f5f8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3042
-682
lines changed

docs/development/adding-models.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ Create the following for each model:
7676

7777
## 13. GraphQL API components
7878

79-
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
79+
Create the following for each model:
8080

81-
**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["policy"])` or similar.
81+
* GraphQL object type for the model in `graphql/types.py` (subclass the appropriate class from `netbox.graphql.types`)
82+
* Add a GraphQL filter for the model in `graphql/filters.py`
83+
* Extend the query class for the app in `graphql/schema.py` with the individual object and object list fields
8284

83-
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
85+
**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["foo"])` or similar.
8486

8587
## 14. Add tests
8688

docs/integrations/graphql-api.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ curl -H "Authorization: Token $TOKEN" \
1111
-H "Content-Type: application/json" \
1212
-H "Accept: application/json" \
1313
http://netbox/graphql/ \
14-
--data '{"query": "query {circuit_list(status:\"active\") {cid provider {name}}}"}'
14+
--data '{"query": "query {circuit_list(filters:{status: STATUS_ACTIVE}) {cid provider {name}}}"}'
1515
```
1616

1717
The response will include the requested data formatted as JSON:
@@ -51,19 +51,48 @@ For more detail on constructing GraphQL queries, see the [GraphQL queries docume
5151

5252
## Filtering
5353

54-
The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active:
54+
!!! note "Changed in NetBox v4.3"
55+
The filtering syntax fo the GraphQL API has changed substantially in NetBox v4.3.
56+
57+
Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only active sites:
5558

5659
```
5760
query {
58-
site_list(filters: {region: "us-nc", status: "active"}) {
61+
site_list(
62+
filters: {
63+
status: STATUS_ACTIVE
64+
}
65+
) {
5966
name
6067
}
6168
}
6269
```
63-
In addition, filtering can be done on list of related objects as shown in the following query:
70+
71+
Filters can be combined with logical operators, such as `OR` and `NOT`. For example, the following will return every site that is planned _or_ assigned to a tenant named Foo:
6472

6573
```
66-
{
74+
query {
75+
site_list(
76+
filters: {
77+
status: STATUS_PLANNED,
78+
OR: {
79+
tenant: {
80+
name: {
81+
exact: "Foo"
82+
}
83+
}
84+
}
85+
}
86+
) {
87+
name
88+
}
89+
}
90+
```
91+
92+
Filtering can also be applied to related objects. For example, the following query will return only enabled interfaces for each device:
93+
94+
```
95+
query {
6796
device_list {
6897
id
6998
name

docs/plugins/development/filtersets.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Filters & Filter Sets
22

3-
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI, REST API, or GraphQL API. NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets.
3+
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets.
44

55
## FilterSet Classes
66

netbox/circuits/graphql/enums.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import strawberry
2+
3+
from circuits.choices import *
4+
5+
__all__ = (
6+
'CircuitStatusEnum',
7+
'CircuitCommitRateEnum',
8+
'CircuitTerminationSideEnum',
9+
'CircuitTerminationPortSpeedEnum',
10+
'CircuitPriorityEnum',
11+
'VirtualCircuitTerminationRoleEnum',
12+
)
13+
14+
15+
CircuitCommitRateEnum = strawberry.enum(CircuitCommitRateChoices.as_enum())
16+
CircuitPriorityEnum = strawberry.enum(CircuitPriorityChoices.as_enum())
17+
CircuitStatusEnum = strawberry.enum(CircuitStatusChoices.as_enum())
18+
CircuitTerminationSideEnum = strawberry.enum(CircuitTerminationSideChoices.as_enum())
19+
CircuitTerminationPortSpeedEnum = strawberry.enum(CircuitTerminationPortSpeedChoices.as_enum())
20+
VirtualCircuitTerminationRoleEnum = strawberry.enum(VirtualCircuitTerminationRoleChoices.as_enum())
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from dataclasses import dataclass
2+
from typing import Annotated, TYPE_CHECKING
3+
4+
import strawberry
5+
import strawberry_django
6+
7+
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
8+
9+
if TYPE_CHECKING:
10+
from netbox.graphql.enums import ColorEnum
11+
12+
__all__ = (
13+
'BaseCircuitTypeFilterMixin',
14+
)
15+
16+
17+
@dataclass
18+
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
19+
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()

netbox/circuits/graphql/filters.py

Lines changed: 149 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
1+
from datetime import date
2+
from typing import Annotated, TYPE_CHECKING
3+
4+
import strawberry
15
import strawberry_django
6+
from strawberry.scalars import ID
7+
from strawberry_django import FilterLookup, DateFilterLookup
8+
9+
from circuits import models
10+
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
11+
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
12+
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
13+
from netbox.graphql.filter_mixins import (
14+
DistanceFilterMixin,
15+
ImageAttachmentFilterMixin,
16+
OrganizationalModelFilterMixin,
17+
PrimaryModelFilterMixin,
18+
)
19+
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
20+
from .filter_mixins import BaseCircuitTypeFilterMixin
221

3-
from circuits import filtersets, models
4-
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
22+
if TYPE_CHECKING:
23+
from core.graphql.filters import ContentTypeFilter
24+
from dcim.graphql.filters import InterfaceFilter
25+
from ipam.graphql.filters import ASNFilter
26+
from netbox.graphql.filter_lookups import IntegerLookup
27+
from .enums import *
528

629
__all__ = (
730
'CircuitFilter',
@@ -19,66 +42,160 @@
1942

2043

2144
@strawberry_django.filter(models.CircuitTermination, lookups=True)
22-
@autotype_decorator(filtersets.CircuitTerminationFilterSet)
23-
class CircuitTerminationFilter(BaseFilterMixin):
24-
pass
45+
class CircuitTerminationFilter(
46+
BaseObjectTypeFilterMixin,
47+
CustomFieldsFilterMixin,
48+
TagsFilterMixin,
49+
ChangeLogFilterMixin,
50+
CabledObjectModelFilterMixin,
51+
):
52+
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
53+
strawberry_django.filter_field()
54+
)
55+
term_side: Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
56+
strawberry_django.filter_field()
57+
)
58+
termination_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
59+
strawberry_django.filter_field()
60+
)
61+
termination_id: ID | None = strawberry_django.filter_field()
62+
port_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
63+
strawberry_django.filter_field()
64+
)
65+
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
66+
strawberry_django.filter_field()
67+
)
68+
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field()
69+
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
70+
description: FilterLookup[str] | None = strawberry_django.filter_field()
2571

2672

2773
@strawberry_django.filter(models.Circuit, lookups=True)
28-
@autotype_decorator(filtersets.CircuitFilterSet)
29-
class CircuitFilter(BaseFilterMixin):
30-
pass
74+
class CircuitFilter(
75+
ContactFilterMixin,
76+
ImageAttachmentFilterMixin,
77+
DistanceFilterMixin,
78+
TenancyFilterMixin,
79+
PrimaryModelFilterMixin
80+
):
81+
cid: FilterLookup[str] | None = strawberry_django.filter_field()
82+
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
83+
strawberry_django.filter_field()
84+
)
85+
provider_id: ID | None = strawberry_django.filter_field()
86+
provider_account: Annotated['ProviderAccountFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
87+
strawberry_django.filter_field()
88+
)
89+
provider_account_id: ID | None = strawberry_django.filter_field()
90+
type: Annotated['CircuitTypeFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
91+
strawberry_django.filter_field()
92+
)
93+
type_id: ID | None = strawberry_django.filter_field()
94+
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
95+
strawberry_django.filter_field()
96+
)
97+
install_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
98+
termination_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
99+
commit_rate: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
100+
strawberry_django.filter_field()
101+
)
31102

32103

33104
@strawberry_django.filter(models.CircuitType, lookups=True)
34-
@autotype_decorator(filtersets.CircuitTypeFilterSet)
35-
class CircuitTypeFilter(BaseFilterMixin):
105+
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
36106
pass
37107

38108

39109
@strawberry_django.filter(models.CircuitGroup, lookups=True)
40-
@autotype_decorator(filtersets.CircuitGroupFilterSet)
41-
class CircuitGroupFilter(BaseFilterMixin):
110+
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
42111
pass
43112

44113

45114
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
46-
@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet)
47-
class CircuitGroupAssignmentFilter(BaseFilterMixin):
48-
pass
115+
class CircuitGroupAssignmentFilter(
116+
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
117+
):
118+
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
119+
strawberry_django.filter_field()
120+
)
121+
member_id: ID | None = strawberry_django.filter_field()
122+
group: Annotated['CircuitGroupFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
123+
strawberry_django.filter_field()
124+
)
125+
group_id: ID | None = strawberry_django.filter_field()
126+
priority: Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
127+
strawberry_django.filter_field()
128+
)
49129

50130

51131
@strawberry_django.filter(models.Provider, lookups=True)
52-
@autotype_decorator(filtersets.ProviderFilterSet)
53-
class ProviderFilter(BaseFilterMixin):
54-
pass
132+
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
133+
name: FilterLookup[str] | None = strawberry_django.filter_field()
134+
slug: FilterLookup[str] | None = strawberry_django.filter_field()
135+
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
55136

56137

57138
@strawberry_django.filter(models.ProviderAccount, lookups=True)
58-
@autotype_decorator(filtersets.ProviderAccountFilterSet)
59-
class ProviderAccountFilter(BaseFilterMixin):
60-
pass
139+
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
140+
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
141+
strawberry_django.filter_field()
142+
)
143+
provider_id: ID | None = strawberry_django.filter_field()
144+
account: FilterLookup[str] | None = strawberry_django.filter_field()
145+
name: FilterLookup[str] | None = strawberry_django.filter_field()
61146

62147

63148
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
64-
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
65-
class ProviderNetworkFilter(BaseFilterMixin):
66-
pass
149+
class ProviderNetworkFilter(PrimaryModelFilterMixin):
150+
name: FilterLookup[str] | None = strawberry_django.filter_field()
151+
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
152+
strawberry_django.filter_field()
153+
)
154+
provider_id: ID | None = strawberry_django.filter_field()
155+
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
67156

68157

69158
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
70-
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
71-
class VirtualCircuitTypeFilter(BaseFilterMixin):
159+
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
72160
pass
73161

74162

75163
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
76-
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
77-
class VirtualCircuitFilter(BaseFilterMixin):
78-
pass
164+
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
165+
cid: FilterLookup[str] | None = strawberry_django.filter_field()
166+
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
167+
strawberry_django.filter_field()
168+
)
169+
provider_network_id: ID | None = strawberry_django.filter_field()
170+
provider_account: Annotated['ProviderAccountFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
171+
strawberry_django.filter_field()
172+
)
173+
provider_account_id: ID | None = strawberry_django.filter_field()
174+
type: Annotated['VirtualCircuitTypeFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
175+
strawberry_django.filter_field()
176+
)
177+
type_id: ID | None = strawberry_django.filter_field()
178+
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
179+
strawberry_django.filter_field()
180+
)
181+
group_assignments: Annotated['CircuitGroupAssignmentFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
182+
strawberry_django.filter_field()
183+
)
79184

80185

81186
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
82-
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
83-
class VirtualCircuitTerminationFilter(BaseFilterMixin):
84-
pass
187+
class VirtualCircuitTerminationFilter(
188+
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
189+
):
190+
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
191+
strawberry_django.filter_field()
192+
)
193+
virtual_circuit_id: ID | None = strawberry_django.filter_field()
194+
role: Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
195+
strawberry_django.filter_field()
196+
)
197+
interface: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
198+
strawberry_django.filter_field()
199+
)
200+
interface_id: ID | None = strawberry_django.filter_field()
201+
description: FilterLookup[str] | None = strawberry_django.filter_field()

netbox/circuits/graphql/types.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Annotated, List, Union
1+
from typing import Annotated, List, TYPE_CHECKING, Union
22

33
import strawberry
44
import strawberry_django
@@ -10,11 +10,15 @@
1010
from tenancy.graphql.types import TenantType
1111
from .filters import *
1212

13+
if TYPE_CHECKING:
14+
from dcim.graphql.types import InterfaceType, LocationType, RegionType, SiteGroupType, SiteType
15+
from ipam.graphql.types import ASNType
16+
1317
__all__ = (
14-
'CircuitTerminationType',
15-
'CircuitType',
1618
'CircuitGroupAssignmentType',
1719
'CircuitGroupType',
20+
'CircuitTerminationType',
21+
'CircuitType',
1822
'CircuitTypeType',
1923
'ProviderType',
2024
'ProviderAccountType',
@@ -62,7 +66,7 @@ class ProviderNetworkType(NetBoxObjectType):
6266

6367
@strawberry_django.type(
6468
models.CircuitTermination,
65-
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
69+
exclude=['termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'],
6670
filters=CircuitTerminationFilter
6771
)
6872
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
@@ -117,7 +121,7 @@ class CircuitGroupType(OrganizationalObjectType):
117121

118122
@strawberry_django.type(
119123
models.CircuitGroupAssignment,
120-
exclude=('member_type', 'member_id'),
124+
exclude=['member_type', 'member_id'],
121125
filters=CircuitGroupAssignmentFilter
122126
)
123127
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):

0 commit comments

Comments
 (0)