diff --git a/backend/infrahub/graphql/mutations/node_getter/by_hfid.py b/backend/infrahub/graphql/mutations/node_getter/by_hfid.py index 2819be07ee..736e18778a 100644 --- a/backend/infrahub/graphql/mutations/node_getter/by_hfid.py +++ b/backend/infrahub/graphql/mutations/node_getter/by_hfid.py @@ -5,6 +5,7 @@ from infrahub.core.branch import Branch from infrahub.core.manager import NodeManager from infrahub.core.node import Node +from infrahub.core.registry import registry from infrahub.core.schema import MainSchemaTypes from infrahub.database import InfrahubDatabase @@ -23,14 +24,61 @@ async def get_node( branch: Branch, at: str, ) -> Optional[Node]: - node = None - if not node_schema.human_friendly_id or "hfid" not in data: - return node - - return await self.node_manager.get_one_by_hfid( - db=self.db, - hfid=data["hfid"], - kind=node_schema.kind, - branch=branch, - at=at, - ) + if not node_schema.human_friendly_id: + return None + + if "hfid" in data: + return await self.node_manager.get_one_by_hfid( + db=self.db, + hfid=data["hfid"], + kind=node_schema.kind, + branch=branch, + at=at, + ) + + for component in node_schema.human_friendly_id: + name = component.split("__")[0] + if name not in data.keys(): + # The update neither includes "hfid" or all components to form an hfid: + return None + + schema_branch = registry.schema.get_schema_branch(name=branch.name) + hfid: list[str] = [] + for component in node_schema.human_friendly_id: + attribute_path = node_schema.parse_schema_path(path=component, schema=schema_branch) + + if attribute_path.is_type_attribute and attribute_path.attribute_schema: + hfid_component = data[attribute_path.attribute_schema.name].get(attribute_path.attribute_property_name) + if hfid_component is not None: + hfid.append(hfid_component) + if ( + attribute_path.relationship_schema + and attribute_path.related_schema + and attribute_path.attribute_property_name + and attribute_path.attribute_schema + ): + related_node = await self.node_manager.find_object( + db=self.db, + kind=attribute_path.relationship_schema.peer, + branch=branch, + at=at, + id=data[attribute_path.relationship_schema.name].get("id"), + hfid=data[attribute_path.relationship_schema.name].get("hfid"), + ) + relationship_attribute = getattr(related_node, attribute_path.attribute_schema.name) + relationship_attribute_value = getattr(relationship_attribute, attribute_path.attribute_property_name) + if relationship_attribute_value is None: + return None + + hfid.append(str(relationship_attribute_value)) + + if len(hfid) == len(node_schema.human_friendly_id): + return await self.node_manager.get_one_by_hfid( + db=self.db, + hfid=hfid, + kind=node_schema.kind, + branch=branch, + at=at, + ) + + return None diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 14e30a6cc0..c1255f0cea 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -345,6 +345,7 @@ async def animal_person_schema_unregistered(db: InfrahubDatabase, node_group_sch "name": "Person", "namespace": "Test", "display_labels": ["name__value"], + "default_filter": "name__value", "human_friendly_id": ["name__value"], "attributes": [ {"name": "name", "kind": "Text", "unique": True}, diff --git a/backend/tests/unit/graphql/test_mutation_upsert.py b/backend/tests/unit/graphql/test_mutation_upsert.py index 928cc76bdb..0e7a01efc3 100644 --- a/backend/tests/unit/graphql/test_mutation_upsert.py +++ b/backend/tests/unit/graphql/test_mutation_upsert.py @@ -310,3 +310,86 @@ async def test_with_hfid_new(db: InfrahubDatabase, default_branch, animal_person "id": new_id, "name": {"value": "Bella"}, } + + +async def test_with_constructed_hfid(db: InfrahubDatabase, default_branch, animal_person_schema) -> None: + """Validate that we can construct an HFID out of the payload without specifying all parts.""" + person_schema = animal_person_schema.get(name="TestPerson") + + person1 = await Node.init(db=db, schema=person_schema, branch=default_branch) + await person1.new(db=db, name="John Snow") + await person1.save(db=db) + + query = """ + mutation UpsertWolf($owner: String!, $weight: BigInt!) { + TestDogUpsert(data: { + name: { value: "Ghost" }, + breed: { value: "Direwolf" }, + color: { value: "White" }, + owner: { id: $owner }, + weight: { value: $weight } + }) { + ok + object { + id + name { + value + } + color { + value + } + breed { + value + } + weight { + value + } + } + } + } + """ + gql_params = prepare_graphql_params(db=db, include_subscription=False, branch=default_branch) + + # Create initial node + initial_weight = 14 + create_result = await graphql( + schema=gql_params.schema, + source=query, + context_value=gql_params.context, + root_value=None, + variable_values={"owner": "John Snow", "weight": initial_weight}, + ) + + # Update previously created node + updated_weight = 68 + update_result = await graphql( + schema=gql_params.schema, + source=query, + context_value=gql_params.context, + root_value=None, + variable_values={"owner": "John Snow", "weight": updated_weight}, + ) + + assert create_result.errors is None + assert create_result.data + assert create_result.data["TestDogUpsert"]["ok"] is True + ghost_id = create_result.data["TestDogUpsert"]["object"]["id"] + assert create_result.data["TestDogUpsert"]["object"] == { + "breed": {"value": "Direwolf"}, + "color": {"value": "White"}, + "id": ghost_id, + "name": {"value": "Ghost"}, + "weight": {"value": initial_weight}, + } + + assert update_result.errors is None + assert update_result.data + assert update_result.data["TestDogUpsert"]["ok"] is True + assert ghost_id == update_result.data["TestDogUpsert"]["object"]["id"] + assert update_result.data["TestDogUpsert"]["object"] == { + "breed": {"value": "Direwolf"}, + "color": {"value": "White"}, + "id": ghost_id, + "name": {"value": "Ghost"}, + "weight": {"value": updated_weight}, + } diff --git a/changelog/4167.added.md b/changelog/4167.added.md new file mode 100644 index 0000000000..f9b8859192 --- /dev/null +++ b/changelog/4167.added.md @@ -0,0 +1 @@ +Add ability to construct HFIDs from payload for upsert mutations diff --git a/python_sdk/infrahub_sdk/node.py b/python_sdk/infrahub_sdk/node.py index 66b9bb0ff3..b75ee77e25 100644 --- a/python_sdk/infrahub_sdk/node.py +++ b/python_sdk/infrahub_sdk/node.py @@ -725,12 +725,11 @@ def get_human_friendly_id(self) -> Optional[list[str]]: if not self._schema.human_friendly_id: return None - # If all components of an HFID are null, we cannot identify a single node + # If an HFID component is missing we assume that it is invalid and not usable for this node hfid_components = [self.get_path_value(path=item) for item in self._schema.human_friendly_id] - if all(c is None for c in hfid_components): + if None in hfid_components: return None - - return [str(c) for c in hfid_components] + return [str(hfid) for hfid in hfid_components] def get_human_friendly_id_as_string(self, include_kind: bool = False) -> Optional[str]: hfid = self.get_human_friendly_id()