mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on GraphQL
This commit is contained in:
@ -18,6 +18,10 @@ django-debug-toolbar
|
||||
# https://github.com/carltongibson/django-filter
|
||||
django-filter
|
||||
|
||||
# Django debug toolbar extension with support for GraphiQL
|
||||
# https://github.com/flavors/django-graphiql-debug-toolbar/
|
||||
django-graphiql-debug-toolbar
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt
|
||||
django-mptt
|
||||
@ -54,6 +58,10 @@ djangorestframework
|
||||
# https://github.com/axnsan12/drf-yasg
|
||||
drf-yasg[validation]
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django
|
||||
graphene_django
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://gunicorn.org/
|
||||
gunicorn
|
||||
|
21
netbox/circuits/graphql/schema.py
Normal file
21
netbox/circuits/graphql/schema.py
Normal file
@ -0,0 +1,21 @@
|
||||
import graphene
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuits = ObjectListField(CircuitType)
|
||||
|
||||
circuit_termination = ObjectField(CircuitTerminationType)
|
||||
circuit_terminations = ObjectListField(CircuitTerminationType)
|
||||
|
||||
circuit_type = ObjectField(CircuitTypeType)
|
||||
circuit_types = ObjectListField(CircuitTypeType)
|
||||
|
||||
provider = ObjectField(ProviderType)
|
||||
providers = ObjectListField(ProviderType)
|
||||
|
||||
provider_network = ObjectField(ProviderNetworkType)
|
||||
provider_networks = ObjectListField(ProviderNetworkType)
|
54
netbox/circuits/graphql/types.py
Normal file
54
netbox/circuits/graphql/types.py
Normal file
@ -0,0 +1,54 @@
|
||||
from circuits import filtersets, models
|
||||
from netbox.graphql.types import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitType',
|
||||
'CircuitTerminationType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderNetworkType',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Object types
|
||||
#
|
||||
|
||||
class ProviderType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Provider
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
class ProviderNetworkType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderNetwork
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
|
||||
class CircuitType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
class CircuitTypeType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitType
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
class CircuitTerminationType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitTermination
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
11
netbox/netbox/graphql/__init__.py
Normal file
11
netbox/netbox/graphql/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
import graphene
|
||||
from graphene_django.converter import convert_django_field
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
|
||||
@convert_django_field.register(TaggableManager)
|
||||
def convert_field_to_tags_list(field, registry=None):
|
||||
"""
|
||||
Register conversion handler for django-taggit's TaggableManager
|
||||
"""
|
||||
return graphene.List(graphene.String)
|
65
netbox/netbox/graphql/fields.py
Normal file
65
netbox/netbox/graphql/fields.py
Normal file
@ -0,0 +1,65 @@
|
||||
from functools import partial
|
||||
|
||||
import graphene
|
||||
from graphene_django import DjangoListField
|
||||
|
||||
from .utils import get_graphene_type
|
||||
|
||||
__all__ = (
|
||||
'ObjectField',
|
||||
'ObjectListField',
|
||||
)
|
||||
|
||||
|
||||
class ObjectField(graphene.Field):
|
||||
"""
|
||||
Retrieve a single object, identified by its numeric ID.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
if 'id' not in kwargs:
|
||||
kwargs['id'] = graphene.Int(required=True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def object_resolver(django_object_type, root, info, **args):
|
||||
"""
|
||||
Return an object given its numeric ID.
|
||||
"""
|
||||
manager = django_object_type._meta.model._default_manager
|
||||
queryset = django_object_type.get_queryset(manager, info)
|
||||
|
||||
return queryset.get(**args)
|
||||
|
||||
def get_resolver(self, parent_resolver):
|
||||
return partial(self.object_resolver, self._type)
|
||||
|
||||
|
||||
class ObjectListField(DjangoListField):
|
||||
"""
|
||||
Retrieve a list of objects, optionally filtered by one or more FilterSet filters.
|
||||
"""
|
||||
def __init__(self, _type, *args, **kwargs):
|
||||
|
||||
assert hasattr(_type._meta, 'filterset_class'), "DjangoFilterListField must define filterset_class under Meta"
|
||||
filterset_class = _type._meta.filterset_class
|
||||
|
||||
# Get FilterSet kwargs
|
||||
filter_kwargs = {}
|
||||
for filter_name, filter_field in filterset_class.get_filters().items():
|
||||
field_type = get_graphene_type(type(filter_field))
|
||||
filter_kwargs[filter_name] = graphene.Argument(field_type)
|
||||
|
||||
super().__init__(_type, args=filter_kwargs, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def list_resolver(django_object_type, resolver, default_manager, root, info, **args):
|
||||
# Get the QuerySet from the object type
|
||||
queryset = django_object_type.get_queryset(default_manager, info)
|
||||
|
||||
# Instantiate and apply the FilterSet
|
||||
filterset_class = django_object_type._meta.filterset_class
|
||||
filterset = filterset_class(data=args, queryset=queryset, request=info.context)
|
||||
|
||||
return filterset.qs
|
13
netbox/netbox/graphql/schema.py
Normal file
13
netbox/netbox/graphql/schema.py
Normal file
@ -0,0 +1,13 @@
|
||||
import graphene
|
||||
|
||||
from circuits.graphql.schema import CircuitsQuery
|
||||
|
||||
|
||||
class Query(
|
||||
CircuitsQuery,
|
||||
graphene.ObjectType
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query, auto_camelcase=False)
|
41
netbox/netbox/graphql/types.py
Normal file
41
netbox/netbox/graphql/types.py
Normal file
@ -0,0 +1,41 @@
|
||||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
__all__ = (
|
||||
'BaseObjectType',
|
||||
'ObjectType',
|
||||
'TaggedObjectType',
|
||||
)
|
||||
|
||||
|
||||
class BaseObjectType(DjangoObjectType):
|
||||
"""
|
||||
Base GraphQL object type for all NetBox objects
|
||||
"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
# Enforce object permissions on the queryset
|
||||
return queryset.restrict(info.context.user, 'view')
|
||||
|
||||
|
||||
class ObjectType(BaseObjectType):
|
||||
# TODO: Custom fields support
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class TaggedObjectType(ObjectType):
|
||||
"""
|
||||
Extends ObjectType with support for Tags
|
||||
"""
|
||||
tags = graphene.List(graphene.String)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def resolve_tags(self, info):
|
||||
return self.tags.all()
|
25
netbox/netbox/graphql/utils.py
Normal file
25
netbox/netbox/graphql/utils.py
Normal file
@ -0,0 +1,25 @@
|
||||
import graphene
|
||||
from django_filters import filters
|
||||
|
||||
|
||||
def get_graphene_type(filter_cls):
|
||||
"""
|
||||
Return the appropriate Graphene scalar type for a django_filters Filter
|
||||
"""
|
||||
if issubclass(filter_cls, filters.BooleanFilter):
|
||||
field_type = graphene.Boolean
|
||||
elif issubclass(filter_cls, filters.NumberFilter):
|
||||
# TODO: Floats? BigInts?
|
||||
field_type = graphene.Int
|
||||
elif issubclass(filter_cls, filters.DateFilter):
|
||||
field_type = graphene.Date
|
||||
elif issubclass(filter_cls, filters.DateTimeFilter):
|
||||
field_type = graphene.DateTime
|
||||
else:
|
||||
field_type = graphene.String
|
||||
|
||||
# Multi-value filters should be handled as lists
|
||||
if issubclass(filter_cls, filters.MultipleChoiceFilter):
|
||||
return graphene.List(field_type)
|
||||
|
||||
return field_type
|
@ -282,9 +282,11 @@ INSTALLED_APPS = [
|
||||
'cacheops',
|
||||
'corsheaders',
|
||||
'debug_toolbar',
|
||||
'graphiql_debug_toolbar',
|
||||
'django_filters',
|
||||
'django_tables2',
|
||||
'django_prometheus',
|
||||
'graphene_django',
|
||||
'mptt',
|
||||
'rest_framework',
|
||||
'taggit',
|
||||
@ -303,7 +305,7 @@ INSTALLED_APPS = [
|
||||
|
||||
# Middleware
|
||||
MIDDLEWARE = [
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'graphiql_debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
|
@ -4,9 +4,11 @@ from django.urls import path, re_path
|
||||
from django.views.static import serve
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
|
||||
from netbox.api.views import APIRootView, StatusView
|
||||
from netbox.graphql.schema import schema
|
||||
from netbox.views import HomeView, StaticMediaFailureView, SearchView
|
||||
from users.views import LoginView, LogoutView
|
||||
from .admin import admin_site
|
||||
@ -60,6 +62,9 @@ _patterns = [
|
||||
path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
|
||||
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
|
||||
|
||||
# GraphQL
|
||||
path('graphql/', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
|
||||
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
||||
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'),
|
||||
|
1
netbox/project-static/volt
Submodule
1
netbox/project-static/volt
Submodule
Submodule netbox/project-static/volt added at 942aa8c7bd
@ -3,6 +3,7 @@ django-cacheops==6.0
|
||||
django-cors-headers==3.7.0
|
||||
django-debug-toolbar==3.2.1
|
||||
django-filter==2.4.0
|
||||
django-graphiql-debug-toolbar==0.1.4
|
||||
django-mptt==0.12.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.1.0
|
||||
@ -12,6 +13,7 @@ django-taggit==1.4.0
|
||||
django-timezone-field==4.1.2
|
||||
djangorestframework==3.12.4
|
||||
drf-yasg[validation]==1.20.0
|
||||
graphene_django==2.15.0
|
||||
gunicorn==20.1.0
|
||||
Jinja2==3.0.1
|
||||
Markdown==3.3.4
|
||||
|
Reference in New Issue
Block a user