Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions infrahub_sdk/node/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def is_resource_pool(self) -> bool:
def get_raw_graphql_data(self) -> dict | None:
return self._data

def _generate_input_data( # noqa: C901
def _generate_input_data( # noqa: C901, PLR0915
self,
exclude_unmodified: bool = False,
exclude_hfid: bool = False,
Expand Down Expand Up @@ -235,7 +235,10 @@ def _generate_input_data( # noqa: C901
rel: RelatedNodeBase | RelationshipManagerBase = getattr(self, item_name)

if rel_schema.cardinality == RelationshipCardinality.ONE and rel_schema.optional and not rel.initialized:
data[item_name] = None
# Only include None for existing nodes to allow clearing relationships
# For new nodes, omit the field to allow object template defaults to be applied
if self._existing:
data[item_name] = None
continue

if rel is None or not rel.initialized:
Expand Down
37 changes: 34 additions & 3 deletions tests/unit/sdk/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -1365,7 +1365,6 @@ async def test_create_input_data(client, location_schema: NodeSchemaAPI, client_
"name": {"value": "JFK1"},
"description": {"value": "JFK Airport"},
"type": {"value": "SITE"},
"primary_tag": None,
}
}

Expand Down Expand Up @@ -1393,6 +1392,38 @@ async def test_create_input_data_with_dropdown(client, location_schema_with_drop
"description": {"value": "JFK Airport"},
"type": {"value": "SITE"},
"status": {"value": None},
}
}


@pytest.mark.parametrize("client_type", client_types)
async def test_update_input_data_existing_node_with_optional_relationship(
client, location_schema: NodeSchemaAPI, client_type
) -> None:
"""Validate that existing nodes include None for uninitialized optional relationships.

This ensures that we can explicitly clear optional relationships when updating existing nodes.
"""
# Simulate an existing node by including an id
data = {
"id": "existing-node-id",
"name": {"value": "JFK1"},
"description": {"value": "JFK Airport"},
"type": {"value": "SITE"},
}

if client_type == "standard":
node = InfrahubNode(client=client, schema=location_schema, data=data)
else:
node = InfrahubNodeSync(client=client, schema=location_schema, data=data)

# For existing nodes, optional uninitialized relationships should include None
assert node._generate_input_data()["data"] == {
"data": {
"id": "existing-node-id",
"name": {"value": "JFK1"},
"description": {"value": "JFK Airport"},
"type": {"value": "SITE"},
"primary_tag": None,
}
}
Expand Down Expand Up @@ -1641,7 +1672,7 @@ async def test_create_input_data_with_IPHost_attribute(client, ipaddress_schema,
ip_address = InfrahubNodeSync(client=client, schema=ipaddress_schema, data=data)

assert ip_address._generate_input_data()["data"] == {
"data": {"address": {"value": "1.1.1.1/24", "is_protected": True}, "interface": None}
"data": {"address": {"value": "1.1.1.1/24", "is_protected": True}}
}


Expand All @@ -1655,7 +1686,7 @@ async def test_create_input_data_with_IPNetwork_attribute(client, ipnetwork_sche
ip_network = InfrahubNodeSync(client=client, schema=ipnetwork_schema, data=data)

assert ip_network._generate_input_data()["data"] == {
"data": {"network": {"value": "1.1.1.0/24", "is_protected": True}, "site": None}
"data": {"network": {"value": "1.1.1.0/24", "is_protected": True}}
}


Expand Down