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
2022-01-27 14:18:25 -05:00
parent df95115e2e
commit 03ea257711
8 changed files with 124 additions and 31 deletions

View File

@ -0,0 +1,41 @@
# GraphQL API
## Defining the Schema Class
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class. This class must be a subclass of `graphene.ObjectType`.
### Example
```python
# graphql.py
import graphene
from netbox.graphql.fields import ObjectField, ObjectListField
from . import filtersets, models
class MyModelType(graphene.ObjectType):
class Meta:
model = models.MyModel
fields = '__all__'
filterset_class = filtersets.MyModelFilterSet
class MyQuery(graphene.ObjectType):
mymodel = ObjectField(MyModelType)
mymodel_list = ObjectListField(MyModelType)
schema = MyQuery
```
## GraphQL Fields
::: netbox.graphql.fields.ObjectField
selection:
members: false
rendering:
show_source: false
::: netbox.graphql.fields.ObjectListField
selection:
members: false
rendering:
show_source: false

View File

@ -22,7 +22,7 @@ However, keep in mind that each piece of functionality is entirely optional. For
### Plugin Structure ### Plugin Structure
Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin looks something like this: Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin might look something like this:
```no-highlight ```no-highlight
project-name/ project-name/
@ -102,23 +102,24 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
#### PluginConfig Attributes #### PluginConfig Attributes
| Name | Description | | Name | Description |
| ---- |---------------------------------------------------------------------------------------------------------------| |-----------------------|--------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory | | `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin | | `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) | | `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose | | `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author | | `author` | Name of plugin's author |
| `author_email` | Author's public email address | | `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. | | `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user | | `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values | | `default_settings` | A dictionary of configuration parameters and their default values |
| `min_version` | Minimum version of NetBox with which the plugin is compatible | | `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible | | `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware | | `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | | `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | | `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) | | `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.

View File

@ -108,6 +108,7 @@ nav:
- Forms: 'plugins/development/forms.md' - Forms: 'plugins/development/forms.md'
- Filter Sets: 'plugins/development/filtersets.md' - Filter Sets: 'plugins/development/filtersets.md'
- REST API: 'plugins/development/rest-api.md' - REST API: 'plugins/development/rest-api.md'
- GraphQL API: 'plugins/development/graphql.md'
- Background Tasks: 'plugins/development/background-tasks.md' - Background Tasks: 'plugins/development/background-tasks.md'
- Administration: - Administration:
- Authentication: 'administration/authentication.md' - Authentication: 'administration/authentication.md'

View File

@ -14,9 +14,10 @@ from extras.plugins.utils import import_object
# Initialize plugin registry # Initialize plugin registry
registry['plugins'] = { registry['plugins'] = {
'template_extensions': collections.defaultdict(list), 'graphql_schemas': [],
'menu_items': {}, 'menu_items': {},
'preferences': {}, 'preferences': {},
'template_extensions': collections.defaultdict(list),
} }
@ -55,13 +56,15 @@ class PluginConfig(AppConfig):
# Default integration paths. Plugin authors can override these to customize the paths to # Default integration paths. Plugin authors can override these to customize the paths to
# integrated components. # integrated components.
template_extensions = 'template_content.template_extensions' graphql_schema = 'graphql.schema'
menu_items = 'navigation.menu_items' menu_items = 'navigation.menu_items'
template_extensions = 'template_content.template_extensions'
user_preferences = 'preferences.preferences' user_preferences = 'preferences.preferences'
def ready(self): def ready(self):
plugin_name = self.name.rsplit('.', 1)[1]
# Register template content # Register template content (if defined)
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}") template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
if template_extensions is not None: if template_extensions is not None:
register_template_extensions(template_extensions) register_template_extensions(template_extensions)
@ -71,10 +74,14 @@ class PluginConfig(AppConfig):
if menu_items is not None: if menu_items is not None:
register_menu_items(self.verbose_name, menu_items) register_menu_items(self.verbose_name, menu_items)
# Register user preferences # Register GraphQL schema (if defined)
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
if graphql_schema is not None:
register_graphql_schema(graphql_schema)
# Register user preferences (if defined)
user_preferences = import_object(f"{self.__module__}.{self.user_preferences}") user_preferences = import_object(f"{self.__module__}.{self.user_preferences}")
if user_preferences is not None: if user_preferences is not None:
plugin_name = self.name.rsplit('.', 1)[1]
register_user_preferences(plugin_name, user_preferences) register_user_preferences(plugin_name, user_preferences)
@classmethod @classmethod
@ -180,7 +187,7 @@ def register_template_extensions(class_list):
# Validation # Validation
for template_extension in class_list: for template_extension in class_list:
if not inspect.isclass(template_extension): if not inspect.isclass(template_extension):
raise TypeError(f"PluginTemplateExtension class {template_extension} was passes as an instance!") raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
if not issubclass(template_extension, PluginTemplateExtension): if not issubclass(template_extension, PluginTemplateExtension):
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!") raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
if template_extension.model is None: if template_extension.model is None:
@ -254,6 +261,17 @@ def register_menu_items(section_name, class_list):
registry['plugins']['menu_items'][section_name] = class_list registry['plugins']['menu_items'][section_name] = class_list
#
# GraphQL schemas
#
def register_graphql_schema(graphql_schema):
"""
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
"""
registry['plugins']['graphql_schemas'].append(graphql_schema)
# #
# User preferences # User preferences
# #

View File

@ -0,0 +1,21 @@
import graphene
from graphene_django import DjangoObjectType
from netbox.graphql.fields import ObjectField, ObjectListField
from . import models
class DummyModelType(DjangoObjectType):
class Meta:
model = models.DummyModel
fields = '__all__'
class DummyQuery(graphene.ObjectType):
dummymodel = ObjectField(DummyModelType)
dummymodel_list = ObjectListField(DummyModelType)
schema = DummyQuery

View File

@ -7,6 +7,7 @@ from django.urls import reverse
from extras.registry import registry from extras.registry import registry
from extras.tests.dummy_plugin import config as dummy_config from extras.tests.dummy_plugin import config as dummy_config
from netbox.graphql.schema import Query
@skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") @skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
@ -143,3 +144,12 @@ class PluginTest(TestCase):
user_config = {'bar': 456} user_config = {'bar': 456}
DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION) DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
self.assertEqual(user_config['bar'], 456) self.assertEqual(user_config['bar'], 456)
def test_graphql(self):
"""
Validate the registration and operation of plugin-provided GraphQL schemas.
"""
from extras.tests.dummy_plugin.graphql import DummyQuery
self.assertIn(DummyQuery, registry['plugins']['graphql_schemas'])
self.assertTrue(issubclass(Query, DummyQuery))

View File

@ -41,15 +41,14 @@ class ObjectListField(DjangoListField):
Retrieve a list of objects, optionally filtered by one or more FilterSet filters. Retrieve a list of objects, optionally filtered by one or more FilterSet filters.
""" """
def __init__(self, _type, *args, **kwargs): def __init__(self, _type, *args, **kwargs):
filter_kwargs = {}
assert hasattr(_type._meta, 'filterset_class'), "DjangoFilterListField must define filterset_class under Meta"
filterset_class = _type._meta.filterset_class
# Get FilterSet kwargs # Get FilterSet kwargs
filter_kwargs = {} filterset_class = getattr(_type._meta, 'filterset_class', None)
for filter_name, filter_field in filterset_class.get_filters().items(): if filterset_class:
field_type = get_graphene_type(type(filter_field)) for filter_name, filter_field in filterset_class.get_filters().items():
filter_kwargs[filter_name] = graphene.Argument(field_type) field_type = get_graphene_type(type(filter_field))
filter_kwargs[filter_name] = graphene.Argument(field_type)
super().__init__(_type, args=filter_kwargs, *args, **kwargs) super().__init__(_type, args=filter_kwargs, *args, **kwargs)

View File

@ -3,6 +3,7 @@ import graphene
from circuits.graphql.schema import CircuitsQuery from circuits.graphql.schema import CircuitsQuery
from dcim.graphql.schema import DCIMQuery from dcim.graphql.schema import DCIMQuery
from extras.graphql.schema import ExtrasQuery from extras.graphql.schema import ExtrasQuery
from extras.registry import registry
from ipam.graphql.schema import IPAMQuery from ipam.graphql.schema import IPAMQuery
from tenancy.graphql.schema import TenancyQuery from tenancy.graphql.schema import TenancyQuery
from users.graphql.schema import UsersQuery from users.graphql.schema import UsersQuery
@ -19,6 +20,7 @@ class Query(
UsersQuery, UsersQuery,
VirtualizationQuery, VirtualizationQuery,
WirelessQuery, WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas
graphene.ObjectType graphene.ObjectType
): ):
pass pass