Skip to content

Instantly share code, notes, and snippets.

@jessesomerville
Last active January 5, 2024 16:57
Show Gist options
  • Save jessesomerville/9b93dc5f1557d3c71b49e1bdc5d66cb9 to your computer and use it in GitHub Desktop.
Save jessesomerville/9b93dc5f1557d3c71b49e1bdc5d66cb9 to your computer and use it in GitHub Desktop.
Find cyclical object references in a GraphQL Schema
import argparse
from graphql import (
build_schema,
get_named_type,
GraphQLNamedType,
GraphQLSchema,
is_input_object_type,
is_object_type,
)
import networkx as nx
from typing import (
List,
Tuple,
)
NATIVE_OBJECTS = [
"Query",
"Mutation",
"__Schema",
"__Type",
"__TypeKind",
"__Field",
"__InputValue",
"__EnumValue",
"__Directive",
"__DirectiveLocation",
]
GraphEdge = Tuple[GraphQLNamedType, GraphQLNamedType]
def main(schema_file: str):
"""Read in the schema definition and parse it into a usable form."""
with open(schema_file, "r") as f:
schema = build_schema(f.read())
parse_objects(schema)
def parse_objects(schema: GraphQLSchema) -> None:
"""Fetch each object from the schema and parse them.
Args:
schema (GraphQLSchema): The GraphQL schema.
"""
in_obj_edges = []
out_obj_edges = []
for obj in schema.type_map.values():
if is_input_object_type(obj):
in_obj_edges.extend([
(obj, get_named_type(field.type))
for field in obj.fields.values()
if get_named_type(field.type) != obj
])
elif is_object_type(obj) and obj.name not in NATIVE_OBJECTS:
out_obj_edges.extend([
(obj, get_named_type(field.type))
for field in obj.fields.values()
if get_named_type(field.type) != obj
])
print("=========== Input Object Cycles ===========")
print_cycles(in_obj_edges)
print("============== Object Cycles ==============")
print_cycles(out_obj_edges)
def print_cycles(edges: List[GraphEdge]) -> None:
"""Create graphs for schema input/output objects and make them acyclic.
Each graph will either contain output objects + scalars as nodes or
input objects + scalars as nodes. No graph will include both input and
output objects. The edges of the graph represent the hierarchal structure
of the schema. More formally, and out edge from a node is incident to one
of that object's fields and the outdegree of the node is the number of
fields that object has (at a depth of 1).
GraphQL schemas can (and often do) contain circular references.
Example::
type User {
id: ID!
location: Location
}
type Location {
id: ID!
user: User
}
This function prints the cycles in the schema.
Args:
edges (List[GraphEdge]): A list of edges to build the graph from.
"""
G = nx.DiGraph()
G.add_edges_from(edges)
for i in nx.strongly_connected_components(G):
if len(i) > 1:
cycle = ' > '.join([obj.name for obj in i])
print(f"{cycle} > {next(iter(i))}")
print()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("schema_file", help="The schema SDL file")
args = parser.parse_args()
main(args.schema_file)
graphql-core==3.2.3
networkx==3.2.1
> python graph_cycles.py schema.graphql
=========== Input Object Cycles ===========
AOrderByInput > BOrderByInput > COrderByInput > AOrderByInput

============== Object Cycles ==============
BookList > Book > Author > BookList

State > County > School > State
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment