diff --git a/changelog/630.fixed.md b/changelog/630.fixed.md new file mode 100644 index 00000000..34a54db3 --- /dev/null +++ b/changelog/630.fixed.md @@ -0,0 +1 @@ +Fixed SDK including explicit `null` values for uninitialized optional relationships when creating nodes with object templates, which prevented the backend from applying template defaults. diff --git a/infrahub_sdk/node/node.py b/infrahub_sdk/node/node.py index 72624467..16e55ebf 100644 --- a/infrahub_sdk/node/node.py +++ b/infrahub_sdk/node/node.py @@ -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, @@ -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: diff --git a/tests/unit/sdk/test_node.py b/tests/unit/sdk/test_node.py index e4192871..810bd6a6 100644 --- a/tests/unit/sdk/test_node.py +++ b/tests/unit/sdk/test_node.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import inspect import ipaddress from typing import TYPE_CHECKING @@ -16,10 +18,14 @@ ) from infrahub_sdk.node.constants import SAFE_VALUE from infrahub_sdk.node.related_node import RelatedNode, RelatedNodeSync -from infrahub_sdk.schema import GenericSchema, NodeSchemaAPI if TYPE_CHECKING: + from pytest_httpx import HTTPXMock + from infrahub_sdk.client import InfrahubClient, InfrahubClientSync + from infrahub_sdk.schema import GenericSchema, NodeSchemaAPI + + from .conftest import BothClients # type: ignore[attr-defined] @@ -1365,7 +1371,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, } } @@ -1393,6 +1398,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( + clients: BothClients, location_schema: NodeSchemaAPI, client_type: str +) -> 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=clients.standard, schema=location_schema, data=data) + else: + node = InfrahubNodeSync(client=clients.sync, 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, } } @@ -1641,7 +1678,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}} } @@ -1655,7 +1692,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}} }