#20603: Split GraphQL API into v1 & v2

This commit is contained in:
Jeremy Stretch 2025-10-20 11:00:23 -04:00
parent 77c08b7bf9
commit 7d82493052
5 changed files with 80 additions and 8 deletions

View File

@ -1,5 +1,15 @@
# GraphQL API Parameters # GraphQL API Parameters
## GRAPHQL_DEFAULT_VERSION
!!! note "This parameter was introduced in NetBox v4.5."
Default: `1`
Designates the default version of the GraphQL API served by `/graphql/`. To access a specific version, append the version number to the URL, e.g. `/graphql/v2/`.
---
## GRAPHQL_ENABLED ## GRAPHQL_ENABLED
!!! tip "Dynamic Configuration Parameter" !!! tip "Dynamic Configuration Parameter"

View File

@ -1,7 +1,7 @@
import strawberry import strawberry
from django.conf import settings from django.conf import settings
from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.optimizer import DjangoOptimizerExtension
from strawberry.extensions import MaxAliasesLimiter # , SchemaExtension from strawberry.extensions import MaxAliasesLimiter
from strawberry.schema.config import StrawberryConfig from strawberry.schema.config import StrawberryConfig
from circuits.graphql.schema import CircuitsQuery from circuits.graphql.schema import CircuitsQuery
@ -16,9 +16,17 @@ from virtualization.graphql.schema import VirtualizationQuery
from vpn.graphql.schema import VPNQuery from vpn.graphql.schema import VPNQuery
from wireless.graphql.schema import WirelessQuery from wireless.graphql.schema import WirelessQuery
__all__ = (
'Query',
'QueryV1',
'QueryV2',
'schema_v1',
'schema_v2',
)
@strawberry.type @strawberry.type
class Query( class QueryV1(
UsersQuery, UsersQuery,
CircuitsQuery, CircuitsQuery,
CoreQuery, CoreQuery,
@ -31,11 +39,44 @@ class Query(
WirelessQuery, WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas *registry['plugins']['graphql_schemas'], # Append plugin schemas
): ):
"""Query class for GraphQL API v1"""
pass pass
schema = strawberry.Schema( @strawberry.type
query=Query, class QueryV2(
UsersQuery,
CircuitsQuery,
CoreQuery,
DCIMQuery,
ExtrasQuery,
IPAMQuery,
TenancyQuery,
VirtualizationQuery,
VPNQuery,
WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas
):
"""Query class for GraphQL API v2"""
pass
# Expose a default Query class for the configured default GraphQL version
class Query(QueryV2 if settings.GRAPHQL_DEFAULT_VERSION == 2 else QueryV1):
pass
# Generate schemas for both versions of the GraphQL API
schema_v1 = strawberry.Schema(
query=QueryV1,
config=StrawberryConfig(auto_camel_case=False),
extensions=[
DjangoOptimizerExtension(prefetch_custom_queryset=True),
MaxAliasesLimiter(max_alias_count=settings.GRAPHQL_MAX_ALIASES),
]
)
schema_v2 = strawberry.Schema(
query=QueryV2,
config=StrawberryConfig(auto_camel_case=False), config=StrawberryConfig(auto_camel_case=False),
extensions=[ extensions=[
DjangoOptimizerExtension(prefetch_custom_queryset=True), DjangoOptimizerExtension(prefetch_custom_queryset=True),

View File

@ -0,0 +1,16 @@
from django.conf import settings
from netbox.graphql.schema import schema_v1, schema_v2
__all__ = (
'get_default_schema',
)
def get_default_schema():
"""
Returns the GraphQL schema corresponding to the value of the NETBOX_GRAPHQL_DEFAULT_SCHEMA setting.
"""
if settings.GRAPHQL_DEFAULT_VERSION == 2:
return schema_v2
return schema_v1

View File

@ -137,6 +137,7 @@ EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', [
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {}) FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440) FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440)
GRAPHQL_DEFAULT_VERSION = getattr(configuration, 'GRAPHQL_DEFAULT_VERSION', 1)
GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10) GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node()) HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node())
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {}) HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})

View File

@ -6,7 +6,8 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, Spec
from account.views import LoginView, LogoutView from account.views import LoginView, LogoutView
from netbox.api.views import APIRootView, StatusView from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema from netbox.graphql.schema import schema_v1, schema_v2
from netbox.graphql.utils import get_default_schema
from netbox.graphql.views import NetBoxGraphQLView from netbox.graphql.views import NetBoxGraphQLView
from netbox.plugins.urls import plugin_patterns, plugin_api_patterns from netbox.plugins.urls import plugin_patterns, plugin_api_patterns
from netbox.views import HomeView, MediaView, StaticMediaFailureView, SearchView, htmx from netbox.views import HomeView, MediaView, StaticMediaFailureView, SearchView, htmx
@ -40,7 +41,7 @@ _patterns = [
# HTMX views # HTMX views
path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'), path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'),
# API # REST API
path('api/', APIRootView.as_view(), name='api-root'), path('api/', APIRootView.as_view(), name='api-root'),
path('api/circuits/', include('circuits.api.urls')), path('api/circuits/', include('circuits.api.urls')),
path('api/core/', include('core.api.urls')), path('api/core/', include('core.api.urls')),
@ -54,6 +55,7 @@ _patterns = [
path('api/wireless/', include('wireless.api.urls')), path('api/wireless/', include('wireless.api.urls')),
path('api/status/', StatusView.as_view(), name='api-status'), path('api/status/', StatusView.as_view(), name='api-status'),
# REST API schema
path( path(
"api/schema/", "api/schema/",
cache_page(timeout=86400, key_prefix=f"api_schema_{settings.RELEASE.version}")( cache_page(timeout=86400, key_prefix=f"api_schema_{settings.RELEASE.version}")(
@ -64,8 +66,10 @@ _patterns = [
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='api_docs'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='api_docs'),
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'), path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'),
# GraphQL # GraphQL API
path('graphql/', NetBoxGraphQLView.as_view(schema=schema), name='graphql'), path('graphql/', NetBoxGraphQLView.as_view(schema=get_default_schema()), name='graphql'),
path('graphql/v1/', NetBoxGraphQLView.as_view(schema=schema_v1), name='graphql_v1'),
path('graphql/v2/', NetBoxGraphQLView.as_view(schema=schema_v2), name='graphql_v2'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
path('media/<path:path>', MediaView.as_view(), name='media'), path('media/<path:path>', MediaView.as_view(), name='media'),