From 7d824930527711a2779d047e382444cde8fd5ae3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 20 Oct 2025 11:00:23 -0400 Subject: [PATCH] #20603: Split GraphQL API into v1 & v2 --- docs/configuration/graphql-api.md | 10 +++++++ netbox/netbox/graphql/schema.py | 49 ++++++++++++++++++++++++++++--- netbox/netbox/graphql/utils.py | 16 ++++++++++ netbox/netbox/settings.py | 1 + netbox/netbox/urls.py | 12 +++++--- 5 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 netbox/netbox/graphql/utils.py diff --git a/docs/configuration/graphql-api.md b/docs/configuration/graphql-api.md index 2c1a1c33b..e58bb0576 100644 --- a/docs/configuration/graphql-api.md +++ b/docs/configuration/graphql-api.md @@ -1,5 +1,15 @@ # 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 !!! tip "Dynamic Configuration Parameter" diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index c840e769c..70a6ec7bf 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -1,7 +1,7 @@ import strawberry from django.conf import settings from strawberry_django.optimizer import DjangoOptimizerExtension -from strawberry.extensions import MaxAliasesLimiter # , SchemaExtension +from strawberry.extensions import MaxAliasesLimiter from strawberry.schema.config import StrawberryConfig from circuits.graphql.schema import CircuitsQuery @@ -16,9 +16,17 @@ from virtualization.graphql.schema import VirtualizationQuery from vpn.graphql.schema import VPNQuery from wireless.graphql.schema import WirelessQuery +__all__ = ( + 'Query', + 'QueryV1', + 'QueryV2', + 'schema_v1', + 'schema_v2', +) + @strawberry.type -class Query( +class QueryV1( UsersQuery, CircuitsQuery, CoreQuery, @@ -31,11 +39,44 @@ class Query( WirelessQuery, *registry['plugins']['graphql_schemas'], # Append plugin schemas ): + """Query class for GraphQL API v1""" pass -schema = strawberry.Schema( - query=Query, +@strawberry.type +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), extensions=[ DjangoOptimizerExtension(prefetch_custom_queryset=True), diff --git a/netbox/netbox/graphql/utils.py b/netbox/netbox/graphql/utils.py new file mode 100644 index 000000000..b97b8cecc --- /dev/null +++ b/netbox/netbox/graphql/utils.py @@ -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 diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ca7f9c3fa..afb825e5b 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -137,6 +137,7 @@ EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', [ EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {}) 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) HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node()) HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {}) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 90d70a357..437689732 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -6,7 +6,8 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, Spec from account.views import LoginView, LogoutView 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.plugins.urls import plugin_patterns, plugin_api_patterns from netbox.views import HomeView, MediaView, StaticMediaFailureView, SearchView, htmx @@ -40,7 +41,7 @@ _patterns = [ # HTMX views 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/circuits/', include('circuits.api.urls')), path('api/core/', include('core.api.urls')), @@ -54,6 +55,7 @@ _patterns = [ path('api/wireless/', include('wireless.api.urls')), path('api/status/', StatusView.as_view(), name='api-status'), + # REST API schema path( "api/schema/", 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/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'), - # GraphQL - path('graphql/', NetBoxGraphQLView.as_view(schema=schema), name='graphql'), + # GraphQL API + 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 path('media/', MediaView.as_view(), name='media'),