Coverage for graphqler / graph / graph_generator.py: 91%
95 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
1"""GraphGenerator: Creates a networkx graph and stores it in a pickle file for use later on during fuzzing
2The linker does the following:
3- Serialize all the objects (Objects, Queries, Mutations, InputObjects, Enums)
4- Generate a graph of object dependencies
5- Attach queries to the object node
6- Attach mutations related to the object node
8!Note!: We decide to not link object-objects together here as it is not relevant for graph traversal
9"""
11from pathlib import Path
12from graphqler.utils.file_utils import read_yaml_to_dict
13from graphqler import config
14from .node import Node
15from .utils import draw_graph
17import networkx
20class GraphGenerator:
21 def __init__(self, save_path: str):
22 self.save_path = save_path
23 self.compiled_queries_save_path = Path(save_path) / config.COMPILED_QUERIES_FILE_NAME
24 self.compiled_objects_save_path = Path(save_path) / config.COMPILED_OBJECTS_FILE_NAME
25 self.compiled_mutations_save_path = Path(save_path) / config.COMPILED_MUTATIONS_FILE_NAME
26 self.dependency_graph_visualization_save_path = Path(save_path) / config.GRAPH_VISUALIZATION_OUTPUT
28 self.compiled_queries = read_yaml_to_dict(self.compiled_queries_save_path)
29 self.compiled_objects = read_yaml_to_dict(self.compiled_objects_save_path)
30 self.compiled_mutations = read_yaml_to_dict(self.compiled_mutations_save_path)
32 self.dependency_graph = networkx.DiGraph()
34 def get_dependency_graph(self) -> networkx.DiGraph:
35 """Runs the graph generator and returns the graph
37 Returns:
38 networkx.DiGraph: The directed graph
39 """
40 self.run()
41 return self.dependency_graph
43 def draw_dependency_graph(self):
44 """Draws the dependency graph based on the GRAPH_VISUALIZATION_OUTPUT constant"""
45 draw_graph(self.dependency_graph, self.dependency_graph_visualization_save_path)
47 def run(self):
48 """Generates the graph, creating nodes and creating edges between nodes.
49 3 types of nodes (Objects, Queries, Mutations)
50 """
52 """1. Create query nodes"""
53 query_nodes = {}
54 for query_name, query_body in self.compiled_queries.items():
55 query_nodes[query_name] = Node("Query", query_name, query_body)
57 """2. Create mutation nodes"""
58 mutation_nodes = {}
59 for mutation_name, mutation_body in self.compiled_mutations.items():
60 mutation_node = Node("Mutation", mutation_name, mutation_body)
61 mutation_node.set_mutation_type(mutation_body["mutationType"])
62 mutation_nodes[mutation_name] = mutation_node
64 """3. Create object nodes"""
65 object_nodes = {}
66 for object_name, object_body in self.compiled_objects.items():
67 object_nodes[object_name] = Node("Object", object_name, object_body)
69 """4. Add all nodes to the graph"""
70 self.dependency_graph.add_nodes_from(query_nodes.values())
71 self.dependency_graph.add_nodes_from(mutation_nodes.values())
72 self.dependency_graph.add_nodes_from(object_nodes.values())
74 """5. Link objects and mutations together"""
75 self.create_object_mutation_edges(object_nodes, mutation_nodes)
77 """6. Link objects and queries together"""
78 self.create_object_query_edges(object_nodes, query_nodes)
80 def create_object_mutation_edges(self, object_nodes: dict, mutation_nodes: dict):
81 """Updates the dependency graph with edges between objects and mutations. 3 cases:
82 Case 1: M -> O | When object(O) depends on mutation(M), means O has M in its "associatedMutations", weight 100
83 Case 2: O -> M | When mutation(M) depends on object(O), means M has O in its "hardDependsOn", weight 100
84 Case 3: O -> M | When mutation(M) depends on object(O), means M has O in its "softDependsOn", weight 1
86 Args:
87 object_nodes (dict): Mapping of object_name -> object node
88 mutation_nodes (dict): Mapping of mutation name -> mutation node
89 """
90 # Case 1
91 for object_name, object_node in object_nodes.items():
92 object_information = self.compiled_objects[object_name]
93 if not object_information["associatedMutatations"]:
94 continue # skip if this object doesn't have any associated mutations
96 for associated_mutation_name in object_information["associatedMutatations"]:
97 mutation_node = mutation_nodes[associated_mutation_name]
98 self.dependency_graph.add_edge(mutation_node, object_node, weight=100)
100 # Case 2
101 for mutation_name, mutation_node in mutation_nodes.items():
102 mutation_information = self.compiled_mutations[mutation_name]
103 if not mutation_information["hardDependsOn"]:
104 continue # skip if this mutation doesn't have any hardDependsOn
106 if mutation_information["hardDependsOn"]:
107 for input_name, object_name in mutation_information["hardDependsOn"].items():
108 if object_name != "UNKNOWN":
109 object_node = object_nodes[object_name]
110 self.dependency_graph.add_edge(object_node, mutation_node, weight=100)
112 # Case 3
113 for mutation_name, mutation_node in mutation_nodes.items():
114 mutation_information = self.compiled_mutations[mutation_name]
115 if not mutation_information["softDependsOn"]:
116 continue # skip if this mutation doesn't have any hardDependsOn
118 if mutation_information["softDependsOn"]:
119 for input_name, object_name in mutation_information["softDependsOn"].items():
120 if object_name != "UNKNOWN":
121 object_node = object_nodes[object_name]
122 self.dependency_graph.add_edge(object_node, mutation_node, weight=1)
124 def create_object_query_edges(self, object_nodes: dict, query_nodes: dict):
125 """Updates the dependency graph with edges in between objects and queries. 3 cases:
126 Case 1: M -> O | When object(O) is produced by query(Q), means O has Q in its "associatedQueries", weight 100
127 Case 2: O -> Q | When query(Q) depends on object(O), means Q has O in its "hardDependsOn", weight 100
128 Case 3: O -> Q | When query(Q) depends on object(O), means Q has O in its "softDependsOn", weight 1
130 Args:
131 object_nodes (dict): Mapping of object_name -> object node
132 query_nodes (dict): Mapping of query_name -> query node
133 """
134 # Case 1
135 for object_name, object_node in object_nodes.items():
136 object_information = self.compiled_objects[object_name]
137 if not object_information["associatedQueries"]:
138 continue # skip if this object doesn't have any associatedQueries
140 for associated_query_name in object_information["associatedQueries"]:
141 query_node = query_nodes[associated_query_name]
142 self.dependency_graph.add_edge(query_node, object_node, weight=100)
144 # Case 2
145 for query_name, query_node in query_nodes.items():
146 query_information = self.compiled_queries[query_name]
147 if not query_information["hardDependsOn"]:
148 continue # skip if this querry doesn't have any hardDependsOn
150 for input_name, object_name in query_information["hardDependsOn"].items():
151 if object_name != "UNKNOWN":
152 object_node = object_nodes[object_name]
153 self.dependency_graph.add_edge(object_node, query_node, weight=100)
155 # Case 3
156 for query_name, query_node in query_nodes.items():
157 query_information = self.compiled_queries[query_name]
158 if not query_information["softDependsOn"]:
159 continue # skip if this querry doesn't have any hardDependsOn
161 for input_name, object_name in query_information["softDependsOn"].items():
162 if object_name != "UNKNOWN":
163 object_node = object_nodes[object_name]
164 self.dependency_graph.add_edge(object_node, query_node, weight=1)