1313 FragmentSpreadNode ,
1414 GraphQLSchema ,
1515 InlineFragmentNode ,
16+ ListTypeNode ,
1617 NamedTypeNode ,
1718 NonNullTypeNode ,
1819 OperationDefinitionNode ,
1920 OperationType ,
2021 SelectionSetNode ,
22+ TypeNode ,
23+ )
24+ from graphql .language .ast import (
25+ BooleanValueNode ,
26+ ConstListValueNode ,
27+ ConstObjectValueNode ,
28+ EnumValueNode ,
29+ FloatValueNode ,
30+ IntValueNode ,
31+ ListValueNode ,
32+ NullValueNode ,
33+ ObjectValueNode ,
34+ StringValueNode ,
35+ ValueNode ,
36+ VariableNode ,
2137)
2238from infrahub_sdk .analyzer import GraphQLQueryAnalyzer
2339from infrahub_sdk .utils import extract_fields
@@ -91,9 +107,24 @@ class GraphQLSelectionSet:
91107@dataclass
92108class GraphQLArgument :
93109 name : str
94- value : str
110+ value : Any
95111 kind : str
96112
113+ @property
114+ def is_variable (self ) -> bool :
115+ return self .kind == "variable"
116+
117+ @property
118+ def as_variable_name (self ) -> str :
119+ """Return the name without a $ prefix"""
120+ return str (self .value ).removeprefix ("$" )
121+
122+ @property
123+ def fields (self ) -> list [str ]:
124+ if self .kind != "object_value" or not isinstance (self .value , dict ):
125+ return []
126+ return sorted (self .value .keys ())
127+
97128
98129@dataclass
99130class ObjectAccess :
@@ -106,6 +137,9 @@ class GraphQLVariable:
106137 name : str
107138 type : str
108139 required : bool
140+ is_list : bool = False
141+ inner_required : bool = False
142+ default : Any | None = None
109143
110144
111145@dataclass
@@ -266,6 +300,28 @@ def fields_by_kind(self, kind: str) -> list[str]:
266300
267301 return fields
268302
303+ @cached_property
304+ def variables (self ) -> list [GraphQLVariable ]:
305+ """Return input variables defined on the query document
306+
307+ All subqueries will use the same document level queries,
308+ so only the first entry is required
309+ """
310+ if self .queries :
311+ return self .queries [0 ].variables
312+ return []
313+
314+ def required_argument (self , argument : GraphQLArgument ) -> bool :
315+ if not argument .is_variable :
316+ # If the argument isn't a variable it would have been
317+ # statically defined in the input and as such required
318+ return True
319+ for variable in self .variables :
320+ if variable .name == argument .as_variable_name and variable .required :
321+ return True
322+
323+ return False
324+
269325 @cached_property
270326 def top_level_kinds (self ) -> list [str ]:
271327 return [query .infrahub_model .kind for query in self .queries if query .infrahub_model ]
@@ -298,6 +354,22 @@ def kind_action_map(self) -> dict[str, set[MutateAction]]:
298354
299355 return access
300356
357+ @property
358+ def only_has_unique_targets (self ) -> bool :
359+ """Indicate if the query document is defined so that it will return a single root level object"""
360+ for query in self .queries :
361+ targets_single_query = False
362+ if query .infrahub_model and query .infrahub_model .uniqueness_constraints :
363+ for argument in query .arguments :
364+ if [[argument .name ]] == query .infrahub_model .uniqueness_constraints :
365+ if self .required_argument (argument = argument ):
366+ targets_single_query = True
367+
368+ if not targets_single_query :
369+ return False
370+
371+ return True
372+
301373
302374class InfrahubGraphQLQueryAnalyzer (GraphQLQueryAnalyzer ):
303375 def __init__ (
@@ -603,31 +675,80 @@ def _get_selections(selection_set: SelectionSetNode) -> GraphQLSelectionSet:
603675 ],
604676 )
605677
606- @staticmethod
607- def _get_variables (operation : OperationDefinitionNode ) -> list [GraphQLVariable ]:
608- variables = []
609- for variable in operation .variable_definitions :
610- if isinstance (variable .type , NamedTypeNode ):
611- variables .append (
612- GraphQLVariable (name = variable .variable .name .value , type = variable .type .name .value , required = False )
678+ def _get_variables (self , operation : OperationDefinitionNode ) -> list [GraphQLVariable ]:
679+ variables : list [GraphQLVariable ] = []
680+
681+ for variable in operation .variable_definitions or []:
682+ type_node : TypeNode = variable .type
683+ required = False
684+ is_list = False
685+ inner_required = False
686+
687+ if isinstance (type_node , NonNullTypeNode ):
688+ required = True
689+ type_node = type_node .type
690+
691+ if isinstance (type_node , ListTypeNode ):
692+ is_list = True
693+ inner_type = type_node .type
694+
695+ if isinstance (inner_type , NonNullTypeNode ):
696+ inner_required = True
697+ inner_type = inner_type .type
698+
699+ if isinstance (inner_type , NamedTypeNode ):
700+ type_name = inner_type .name .value
701+ else :
702+ raise TypeError (f"Unsupported inner type node: { inner_type } " )
703+ elif isinstance (type_node , NamedTypeNode ):
704+ type_name = type_node .name .value
705+ else :
706+ raise TypeError (f"Unsupported type node: { type_node } " )
707+
708+ variables .append (
709+ GraphQLVariable (
710+ name = variable .variable .name .value ,
711+ type = type_name ,
712+ required = required ,
713+ is_list = is_list ,
714+ inner_required = inner_required ,
715+ default = self ._parse_value (variable .default_value ) if variable .default_value else None ,
613716 )
614- elif isinstance (variable .type , NonNullTypeNode ):
615- if isinstance (variable .type .type , NamedTypeNode ):
616- variables .append (
617- GraphQLVariable (
618- name = variable .variable .name .value , type = variable .type .type .name .value , required = True
619- )
620- )
717+ )
621718
622719 return variables
623720
624- @staticmethod
625- def _parse_arguments (field_node : FieldNode ) -> list [GraphQLArgument ]:
721+ def _parse_arguments (self , field_node : FieldNode ) -> list [GraphQLArgument ]:
626722 return [
627723 GraphQLArgument (
628724 name = argument .name .value ,
629- value = getattr (argument .value , "value" , "" ),
725+ value = self . _parse_value (argument .value ),
630726 kind = argument .value .kind ,
631727 )
632728 for argument in field_node .arguments
633729 ]
730+
731+ def _parse_value (self , node : ValueNode ) -> Any :
732+ match node :
733+ case VariableNode ():
734+ value : Any = f"${ node .name .value } "
735+ case IntValueNode ():
736+ value = int (node .value )
737+ case FloatValueNode ():
738+ value = float (node .value )
739+ case StringValueNode ():
740+ value = node .value
741+ case BooleanValueNode ():
742+ value = node .value
743+ case NullValueNode ():
744+ value = None
745+ case EnumValueNode ():
746+ value = node .value
747+ case ListValueNode () | ConstListValueNode ():
748+ value = [self ._parse_value (item ) for item in node .values ]
749+ case ObjectValueNode () | ConstObjectValueNode ():
750+ value = {field .name .value : self ._parse_value (field .value ) for field in node .fields }
751+ case _:
752+ raise TypeError (f"Unsupported value node: { node } " )
753+
754+ return value
0 commit comments