netbox
netbox copied to clipboard
9856 Replace graphene with Strawberry
Fixes: #9856
Replaces django-graphene with strawberry-django a new engine for handling GraphQL queries. Notable changes:
- Converts all GraphQL types to Strawberry Types
- Includes much more updated GraphiQL browser
- Decorator to auto-generate Strawberry required data-classes for filters as Strawberry doesn't support django-filter currently.
Example Queries:
{
circuit_termination_list(filters: {cabled: false}) {
id
display
cable {
id
}
}
}
and
{
location_list {
image_attachments {
id
}
contacts {
id
}
changelog {
id
}
display
class_type
tags {
id
}
custom_fields
id
created
last_updated
custom_field_data
name
slug
description
site {
id
}
status
tenant {
id
}
lft
rght
tree_id
level
children {
id
}
powerpanel_set {
id
}
cabletermination_set {
id
}
racks {
id
}
vlan_groups {
id
}
parent {
id
}
devices {
id
}
}
}
Filtering lookups are different between the two libraries:
Graphene
user_list(is_superuser: true) {
id
}
Strawberry
user_list(filters: {is_superuser: true}) {
id
}
The filtering lookup syntax (greater than, less than, case-insensitive compare) is also different: Graphene
{
user_list(username__ic: "bob") {
username
}
}
Strawberry
{
user_list(filters: {username: {i_contains: "bob"}}) {
username
}
}
For stawberry, allowing lookups on fields forces the lookup to be present, so it forces this somewhat awkward syntax: Graphene
{
user_list(id: "3") {
id
}
}
Strawberry
{
user_list(filters: {id: {exact: 3}}) {
id
}
}
Performance optimizer
Below is a ridiculous nested query that can be used to show optimization
old graphene: 55 queries strawberry: 24 queries
{
location_list {
image_attachments {
id
}
contacts {
id
}
changelog {
id
}
display
class_type
tags {
id
}
custom_fields
id
created
last_updated
custom_field_data
name
slug
description
site {
id
name
locations {
id
name
}
}
status
tenant {
id
sites {
id
name
}
}
lft
rght
tree_id
level
children {
id
}
powerpanel_set {
id
}
cabletermination_set {
id
}
racks {
id
site {
id
name
}
}
vlan_groups {
id
}
parent {
id
}
devices {
id
}
}
}
Remaining work:
- [x] fix type definitions
- [x] fix type data types
- [x] fix filtersets
- [x] Auto generate Filtersets definitions
- [x] Choice field handling
- [x] Fix GraphQL tests auto-discovery of fields
- [x] Update netbox/graphql/views.py GraphQLView (GraphiQL)
- [x] netbox/netbox/graphql/fields.py (ObjectField, ObjectListfield)? - I think this is obsolete from optimizer
- [x] netbox/netbox/graphql/scalars.py (BigInt)?
- [x] netbox/netbox/graphql/utils.py (get_graphene_type)?
- [x] netbox/dcim/graphql/gfk_mixins.py (gfk mixins)?
Including some GraphQL queries here I used for testing as they are handy for reference - just got some GFK stuff working! :tada: :
{
circuit_termination_list {
id
link_peers {
__typename
... on InterfaceType {
id
name
}
... on RearPortType {
id
display
device {
pk
}
}
}
}
}
Script for testing match of the schemas (will keep it updated as needed):
# An example to get the remaining rate limit using the Github GraphQL API.
import requests
headers = {"Authorization": "Bearer YOUR API KEY"}
headers = None
new_url = 'http://127.0.0.1:8000/graphql/'
old_url = 'https://demo.netbox.dev/graphql/'
def run_query(url, query): # A simple function to use requests.post to make the API call. Note the json= section.
request = requests.post(url, json={'query': query}, headers=headers)
if request.status_code == 200:
return request.json()
else:
raise Exception("Query failed to run by returning code of {}. {}".format(request.status_code, query))
# The GraphQL query (with a few aditional bits included) itself defined as a multi-line string.
query_schema = """
{
__schema {
queryType {
fields {
name
type {
name
}
}
}
}
}
"""
def get_all_endpoints(url):
query = """
{
__schema {
types {
name
}
}
}
"""
types = run_query(url, query)
types = types["data"]["__schema"]["types"]
all_types = []
for type in types:
name = type["name"]
if name.endswith("Type"):
all_types.append(name)
all_types.sort()
return all_types
new_endpoints = get_all_endpoints(new_url)
old_endpoints = get_all_endpoints(old_url)
print("Main Endpoint Diff")
print(set(old_endpoints) - set(new_endpoints))
print("")
def get_endpoint_types(url, endpoint):
query = f"""
{{
__type(name: "{endpoint}") {{
name
fields {{
name
type {{
name
kind
}}
}}
}}
}}
"""
types = run_query(url, query)
types = types["data"]["__type"]["fields"]
all_types = []
for type in types:
name = type["name"]
all_types.append(name)
all_types.sort()
return all_types
checked = 0
failed = 0
success = 0
for endpoint in new_endpoints:
if endpoint != 'DjangoImageType' and endpoint != 'DjangoModelType':
checked += 1
new_types = get_endpoint_types(new_url, endpoint)
old_types = get_endpoint_types(old_url, endpoint)
diff = set(old_types) - set(new_types)
if diff:
failed += 1
print(endpoint)
print("--------------------")
print(diff)
print("")
else:
success += 1
print(f"checked: {checked}")
print(f"failed: {failed}")
print(f"success: {success}")
Fixed up the GraphiQL view, the old method of bundling the assets wouldn't work as the new GraphiQL has some include packages that ESBuild can't translate to ES6, see: (https://github.com/graphql/graphiql/discussions/2525) - we could switch to webpack which does handle it, but as it is just for GraphiQL it seemed fine to just statically link the separate files. Changed the ESBuild script to copy the files out of the node-packages.
django-graphical-debug-toolbar is no longer needed, so removed that. The strawberry one is handled by middleware already.