Skip to content

Commit 04d80a2

Browse files
authored
Merge pull request #149 from opsmill/lgu-fix-node-hierarchical
Fix GraphQL query sent with hierarchical nodes
2 parents ec61275 + 5ff89a9 commit 04d80a2

File tree

4 files changed

+112
-0
lines changed

4 files changed

+112
-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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,54 @@ 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+
{"name": "name", "kind": "Text", "unique": True, "order_weight": 900},
517+
],
518+
}
519+
],
520+
"nodes": [
521+
{
522+
"name": "Country",
523+
"namespace": "Location",
524+
"description": "A country within a continent.",
525+
"inherit_from": ["LocationGeneric"],
526+
"generate_profile": False,
527+
"default_filter": "name__value",
528+
"order_by": ["name__value"],
529+
"display_labels": ["name__value"],
530+
"children": "LocationSite",
531+
"attributes": [{"name": "shortname", "kind": "Text"}],
532+
},
533+
{
534+
"name": "Site",
535+
"namespace": "Location",
536+
"description": "A site within a country.",
537+
"inherit_from": ["LocationGeneric"],
538+
"default_filter": "name__value",
539+
"order_by": ["name__value"],
540+
"display_labels": ["name__value"],
541+
"children": "",
542+
"parent": "LocationCountry",
543+
"attributes": [{"name": "shortname", "kind": "Text"}],
544+
},
545+
],
546+
}
547+
return schema
548+
549+
502550
class BusRecorder(InfrahubMessageBus):
503551
def __init__(self, component_type: Optional[ComponentType] = None):
504552
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(
299+
self, client: InfrahubClient, db: InfrahubDatabase, init_db_base, base_dataset, hierarchical_schema
300+
):
301+
await client.schema.load(schemas=[hierarchical_schema])
302+
303+
location_country = await client.create(
304+
kind="LocationCountry", name="country_name", shortname="country_shortname"
305+
)
306+
await location_country.save()
307+
308+
location_site = await client.create(
309+
kind="LocationSite", name="site_name", shortname="site_shortname", parent=location_country
310+
)
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,26 @@ 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+
# See issue #148.
300+
def test_hierarchical(
301+
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", name="country_name", shortname="country_shortname")
306+
location_country.save()
307+
308+
location_site = client.create(
309+
kind="LocationSite", name="site_name", shortname="site_shortname", parent=location_country
310+
)
311+
location_site.save()
312+
313+
nodes = 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"

0 commit comments

Comments
 (0)