1
0
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:
jeremystretch
2021-06-08 13:52:39 -04:00
parent 442b3fcc48
commit 6a07f66cfc
12 changed files with 249 additions and 1 deletions

View File

@ -18,6 +18,10 @@ django-debug-toolbar
# https://github.com/carltongibson/django-filter # https://github.com/carltongibson/django-filter
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) # Modified Preorder Tree Traversal (recursive nesting of objects)
# https://github.com/django-mptt/django-mptt # https://github.com/django-mptt/django-mptt
django-mptt django-mptt
@ -54,6 +58,10 @@ djangorestframework
# https://github.com/axnsan12/drf-yasg # https://github.com/axnsan12/drf-yasg
drf-yasg[validation] drf-yasg[validation]
# Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django
graphene_django
# WSGI HTTP server # WSGI HTTP server
# https://gunicorn.org/ # https://gunicorn.org/
gunicorn gunicorn

View 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)

View 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

View 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)

View 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

View 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)

View 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()

View 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

View File

@ -282,9 +282,11 @@ INSTALLED_APPS = [
'cacheops', 'cacheops',
'corsheaders', 'corsheaders',
'debug_toolbar', 'debug_toolbar',
'graphiql_debug_toolbar',
'django_filters', 'django_filters',
'django_tables2', 'django_tables2',
'django_prometheus', 'django_prometheus',
'graphene_django',
'mptt', 'mptt',
'rest_framework', 'rest_framework',
'taggit', 'taggit',
@ -303,7 +305,7 @@ INSTALLED_APPS = [
# Middleware # Middleware
MIDDLEWARE = [ MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware', 'graphiql_debug_toolbar.middleware.DebugToolbarMiddleware',
'django_prometheus.middleware.PrometheusBeforeMiddleware', 'django_prometheus.middleware.PrometheusBeforeMiddleware',
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',

View File

@ -4,9 +4,11 @@ from django.urls import path, re_path
from django.views.static import serve from django.views.static import serve
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.views import get_schema_view 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 extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
from netbox.api.views import APIRootView, StatusView from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema
from netbox.views import HomeView, StaticMediaFailureView, SearchView from netbox.views import HomeView, StaticMediaFailureView, SearchView
from users.views import LoginView, LogoutView from users.views import LoginView, LogoutView
from .admin import admin_site from .admin import admin_site
@ -60,6 +62,9 @@ _patterns = [
path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'), 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'), 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 # Serving static media in Django to pipe it through LoginRequiredMiddleware
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}), path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'), path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'),

Submodule netbox/project-static/volt added at 942aa8c7bd

View File

@ -3,6 +3,7 @@ django-cacheops==6.0
django-cors-headers==3.7.0 django-cors-headers==3.7.0
django-debug-toolbar==3.2.1 django-debug-toolbar==3.2.1
django-filter==2.4.0 django-filter==2.4.0
django-graphiql-debug-toolbar==0.1.4
django-mptt==0.12.0 django-mptt==0.12.0
django-pglocks==1.0.4 django-pglocks==1.0.4
django-prometheus==2.1.0 django-prometheus==2.1.0
@ -12,6 +13,7 @@ django-taggit==1.4.0
django-timezone-field==4.1.2 django-timezone-field==4.1.2
djangorestframework==3.12.4 djangorestframework==3.12.4
drf-yasg[validation]==1.20.0 drf-yasg[validation]==1.20.0
graphene_django==2.15.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.0.1 Jinja2==3.0.1
Markdown==3.3.4 Markdown==3.3.4