Skip to content

Commit 8e85fe6

Browse files
committed
Fix GraphQL query sent by sdk with hierarchical nodes
1 parent 858eeac commit 8e85fe6

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed

infrahub_sdk/node.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,14 @@ async def generate_query_data_node(
12751275

12761276
if rel_schema and rel_schema.cardinality == "one":
12771277
rel_data = RelatedNode._generate_query_data(peer_data=peer_data)
1278+
# Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
1279+
# tries to resolve attributes in this ancestor instead of actual node. To avoid
1280+
# invalid queries issues when attribute is missing in the common ancestor, we use a fragment
1281+
# to explicit actual node kind we are querying.
1282+
if rel_schema.kind == RelationshipKind.HIERARCHY:
1283+
data_node = rel_data["node"]
1284+
rel_data["node"] = {}
1285+
rel_data["node"][f"...on {rel_schema.peer}"] = data_node
12781286
elif rel_schema and rel_schema.cardinality == "many":
12791287
rel_data = RelationshipManager._generate_query_data(peer_data=peer_data)
12801288

@@ -1764,6 +1772,14 @@ def generate_query_data_node(
17641772

17651773
if rel_schema and rel_schema.cardinality == "one":
17661774
rel_data = RelatedNodeSync._generate_query_data(peer_data=peer_data)
1775+
# Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
1776+
# tries to resolve attributes in this ancestor instead of actual node. To avoid
1777+
# invalid queries issues when attribute is missing in the common ancestor, we use a fragment
1778+
# to explicit actual node kind we are querying.
1779+
if rel_schema.kind == RelationshipKind.HIERARCHY:
1780+
data_node = rel_data["node"]
1781+
rel_data["node"] = {}
1782+
rel_data["node"][f"...on {rel_schema.peer}"] = data_node
17671783
elif rel_schema and rel_schema.cardinality == "many":
17681784
rel_data = RelationshipManagerSync._generate_query_data(peer_data=peer_data)
17691785

tests/integration/conftest.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,69 @@ async def ipam_schema() -> SchemaRoot:
499499
return SCHEMA
500500

501501

502+
@pytest.fixture(scope="module")
503+
async def hierarchical_schema() -> dict:
504+
schema = {
505+
"version": "1.0",
506+
"generics": [
507+
{
508+
"name": "Generic",
509+
"namespace": "Location",
510+
"description": "Generic hierarchical location",
511+
"label": "Location",
512+
"hierarchical": True,
513+
"human_friendly_id": ["name__value"],
514+
"include_in_menu": True,
515+
"attributes": [
516+
{
517+
"name": "name",
518+
"kind": "Text",
519+
"unique": True,
520+
"order_weight": 900
521+
},
522+
]
523+
}
524+
],
525+
"nodes": [
526+
{
527+
"name": "Country",
528+
"namespace": "Location",
529+
"description": "A country within a continent.",
530+
"inherit_from": ["LocationGeneric"],
531+
"generate_profile": False,
532+
"default_filter": "name__value",
533+
"order_by": ["name__value"],
534+
"display_labels": ["name__value"],
535+
"children": "LocationSite",
536+
"attributes": [
537+
{
538+
"name": "shortname",
539+
"kind": "Text"
540+
}
541+
]
542+
},
543+
{
544+
"name": "Site",
545+
"namespace": "Location",
546+
"description": "A site within a country.",
547+
"inherit_from": ["LocationGeneric"],
548+
"default_filter": "name__value",
549+
"order_by": ["name__value"],
550+
"display_labels": ["name__value"],
551+
"children": "",
552+
"parent": "LocationCountry",
553+
"attributes": [
554+
{
555+
"name": "shortname",
556+
"kind": "Text"
557+
}
558+
]
559+
}
560+
]
561+
}
562+
return schema
563+
564+
502565
class BusRecorder(InfrahubMessageBus):
503566
def __init__(self, component_type: Optional[ComponentType] = None):
504567
self.messages: list[InfrahubMessage] = []

tests/integration/test_infrahub_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,28 @@ async def test_create_branch(self, client: InfrahubClient, db: InfrahubDatabase,
293293
async def test_create_branch_async(self, client: InfrahubClient, db: InfrahubDatabase, init_db_base, base_dataset):
294294
task_id = await client.branch.create(branch_name="new-branch-2", wait_until_completion=False)
295295
assert isinstance(task_id, str)
296+
297+
# See issue #148.
298+
async def test_hierarchical(self, client: InfrahubClient, db: InfrahubDatabase, init_db_base, base_dataset, hierarchical_schema):
299+
300+
await client.schema.load(schemas=[hierarchical_schema])
301+
302+
location_country = await client.create(kind="LocationCountry",
303+
name="country_name",
304+
shortname="country_shortname")
305+
await location_country.save()
306+
307+
location_site = await client.create(kind="LocationSite",
308+
name="site_name",
309+
shortname="site_shortname",
310+
parent=location_country)
311+
await location_site.save()
312+
313+
nodes = await client.all(kind="LocationSite", prefetch_relationships=True, populate_store=True)
314+
assert len(nodes) == 1
315+
site_node = nodes[0]
316+
assert site_node.name.value == "site_name"
317+
assert site_node.shortname.value == "site_shortname"
318+
country_node = site_node.parent.get()
319+
assert country_node.name.value == "country_name"
320+
assert country_node.shortname.value == "country_shortname"

tests/integration/test_infrahub_client_sync.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,29 @@ def test_create_branch(self, client: InfrahubClientSync, db: InfrahubDatabase, i
295295
def test_create_branch_async(self, client: InfrahubClientSync, db: InfrahubDatabase, init_db_base, base_dataset):
296296
task_id = client.branch.create(branch_name="new-branch-2", wait_until_completion=False)
297297
assert isinstance(task_id, str)
298+
299+
300+
# See issue #148.
301+
def test_hierarchical(self, client: InfrahubClientSync, db: InfrahubDatabase, init_db_base, base_dataset, hierarchical_schema):
302+
303+
client.schema.load(schemas=[hierarchical_schema])
304+
305+
location_country = client.create(kind="LocationCountry",
306+
name="country_name",
307+
shortname="country_shortname")
308+
location_country.save()
309+
310+
location_site = client.create(kind="LocationSite",
311+
name="site_name",
312+
shortname="site_shortname",
313+
parent=location_country)
314+
location_site.save()
315+
316+
nodes = client.all(kind="LocationSite", prefetch_relationships=True, populate_store=True)
317+
assert len(nodes) == 1
318+
site_node = nodes[0]
319+
assert site_node.name.value == "site_name"
320+
assert site_node.shortname.value == "site_shortname"
321+
country_node = site_node.parent.get()
322+
assert country_node.name.value == "country_name"
323+
assert country_node.shortname.value == "country_shortname"

0 commit comments

Comments
 (0)