Coverage for netbox/graphql/types.py: 75%
53 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import strawberry
2import strawberry_django
3from django.contrib.contenttypes.models import ContentType
4from django.db.models import ExpressionWrapper, F, Func, IntegerField, Value
5from strawberry.types import Info
7from core.graphql.mixins import ChangelogMixin
8from core.models import ObjectType as ObjectType_
9from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMixin
10from netbox.graphql.utils import register_model_graphql_type
11from users.graphql.mixins import OwnerMixin
13__all__ = (
14 'BaseObjectType',
15 'ContentTypeType',
16 'LtreeNodeMixin',
17 'NestedGroupObjectType',
18 'NestedLtreeGroupObjectType',
19 'NetBoxObjectType',
20 'ObjectType',
21 'OrganizationalObjectType',
22 'PrimaryObjectType',
23 'register_type',
24)
27def register_type(model, **kwargs):
28 """
29 Drop-in replacement for `strawberry_django.type()` for model-bound NetBox GraphQL output types. Before delegating
30 to `strawberry_django.type()`, any plugin-registered output-type mixins for the given model are spliced into the
31 decorated class's bases. With no extensions registered this is an exact pass-through, leaving schema output
32 unchanged. See `register_model_graphql_type` for the registry-timing contract.
33 """
34 return register_model_graphql_type(model, strawberry_django.type, 'graphql_type_extensions', **kwargs)
37#
38# Base types
39#
41@strawberry.type
42class BaseObjectType:
43 """
44 Base GraphQL object type for all NetBox objects. Restricts the model queryset to enforce object permissions.
45 """
47 @classmethod
48 def get_queryset(cls, queryset, info: Info, **kwargs):
49 # Enforce object permissions on the queryset
50 if hasattr(queryset, 'restrict'):
51 return queryset.restrict(info.context.request.user, 'view')
52 return queryset
54 @strawberry_django.field
55 def display(self) -> str:
56 return str(self)
58 @strawberry_django.field
59 def class_type(self) -> str:
60 return self.__class__.__name__
63class ObjectType(
64 ChangelogMixin,
65 BaseObjectType
66):
67 """
68 Base GraphQL object type for unclassified models which support change logging
69 """
70 pass
73class PrimaryObjectType(
74 ChangelogMixin,
75 CustomFieldsMixin,
76 JournalEntriesMixin,
77 TagsMixin,
78 OwnerMixin,
79 BaseObjectType
80):
81 """
82 Base GraphQL type for models which inherit from PrimaryModel.
83 """
84 pass
87class OrganizationalObjectType(
88 ChangelogMixin,
89 CustomFieldsMixin,
90 JournalEntriesMixin,
91 TagsMixin,
92 OwnerMixin,
93 BaseObjectType
94):
95 """
96 Base GraphQL type for models which inherit from OrganizationalModel.
97 """
98 pass
101@strawberry.type
102class LtreeNodeMixin:
103 """
104 Exposes the ltree-backed tree depth as a `level` field, preserving the `level`
105 field MPTT-based types previously surfaced automatically as a real column.
107 The depth is computed in the database as `nlevel(path) - 1` (root = 0) and
108 annotated onto the queryset. We prefer the annotation over the `path` column,
109 which is excluded from the schema. When a resolution path does not apply the
110 annotation (e.g. a nested relation), `ltree_level` is absent; fall back to the
111 loaded `path` string (the same depth the LtreeModel.level property computes)
112 so the field never raises AttributeError.
113 """
114 @strawberry_django.field(annotate={
115 'ltree_level': ExpressionWrapper(
116 Func(F('path'), function='nlevel', output_field=IntegerField()) - Value(1),
117 output_field=IntegerField(),
118 )
119 })
120 def level(self) -> int:
121 ltree_level = getattr(self, 'ltree_level', None)
122 if ltree_level is not None:
123 return ltree_level
124 path = getattr(self, 'path', '') or ''
125 return str(path).count('.')
128class NestedGroupObjectType(
129 ChangelogMixin,
130 CustomFieldsMixin,
131 JournalEntriesMixin,
132 TagsMixin,
133 OwnerMixin,
134 BaseObjectType
135):
136 """
137 Base GraphQL type for the deprecated MPTT-backed NestedGroupModel, kept for
138 plugin compatibility. MPTT exposes `level` as a real column, so no annotation
139 mixin is needed. New code should use NestedLtreeGroupObjectType.
140 """
141 pass
144class NestedLtreeGroupObjectType(
145 LtreeNodeMixin,
146 ChangelogMixin,
147 CustomFieldsMixin,
148 JournalEntriesMixin,
149 TagsMixin,
150 OwnerMixin,
151 BaseObjectType
152):
153 """
154 Base GraphQL type for models which inherit from NestedLtreeGroupModel.
155 Adds a `level` field annotated via `nlevel(path)`.
156 """
157 pass
160class NetBoxObjectType(
161 ChangelogMixin,
162 CustomFieldsMixin,
163 JournalEntriesMixin,
164 TagsMixin,
165 BaseObjectType
166):
167 pass
170#
171# Miscellaneous types
172#
174@register_type(
175 ContentType,
176 fields=['id', 'app_label', 'model'],
177 pagination=True
178)
179class ContentTypeType:
180 pass
183@register_type(
184 ObjectType_,
185 fields=['id', 'app_label', 'model'],
186 pagination=True
187)
188class ObjectTypeType:
189 pass