> python graph_cycles.py schema.graphql
=========== Input Object Cycles ===========
AOrderByInput > BOrderByInput > COrderByInput > AOrderByInput
============== Object Cycles ==============
BookList > Book > Author > BookList
State > County > School > State
Last active
January 5, 2024 16:57
-
-
Save jessesomerville/9b93dc5f1557d3c71b49e1bdc5d66cb9 to your computer and use it in GitHub Desktop.
Find cyclical object references in a GraphQL Schema
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
graphql-core==3.2.3 | |
networkx==3.2.1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment