Skip to content

Commit

Permalink
Merge pull request #5681 from opsmill/pog-add-node-changelog-parents-…
Browse files Browse the repository at this point in the history
…IFC-1219

Add support to indicate node.parent in the node_changelog
  • Loading branch information
ogenstad authored Feb 6, 2025
2 parents df1bd76 + 5ea1762 commit f4baa7a
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 6 deletions.
67 changes: 63 additions & 4 deletions backend/infrahub/core/changelog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from typing import TYPE_CHECKING, Any, Self, cast
from uuid import UUID

from pydantic import BaseModel, Field, computed_field, field_validator, model_validator
from pydantic import BaseModel, Field, PrivateAttr, computed_field, field_validator, model_validator

from infrahub.core.constants import NULL_VALUE, DiffAction, RelationshipCardinality
from infrahub.core.constants import NULL_VALUE, DiffAction, RelationshipCardinality, RelationshipKind

if TYPE_CHECKING:
from infrahub.core.attribute import BaseAttribute
Expand Down Expand Up @@ -100,6 +100,11 @@ class RelationshipCardinalityOneChangelog(BaseModel):
properties: dict[str, PropertyChangelog] = Field(
default_factory=dict, description="Changes to properties of this relationship if any were made"
)
_parent: ChangelogNodeParent | None = PrivateAttr(default=None)

@property
def parent(self) -> ChangelogNodeParent | None:
return self._parent

@computed_field
def cardinality(self) -> str:
Expand All @@ -119,6 +124,20 @@ def peer_status(self) -> DiffAction:
def add_property(self, name: str, value_current: bool | str | None, value_previous: bool | str | None) -> None:
self.properties[name] = PropertyChangelog(name=name, value=value_current, value_previous=value_previous)

def set_parent(self, parent_id: str, parent_kind: str) -> None:
self._parent = ChangelogNodeParent(node_id=parent_id, node_kind=parent_kind)

def set_parent_from_relationship(self, relationship: Relationship) -> None:
if relationship.schema.kind == RelationshipKind.PARENT:
if (
self.peer_status in [DiffAction.ADDED, DiffAction.UNCHANGED, DiffAction.UPDATED]
and self.peer_id
and self.peer_kind
):
self._parent = ChangelogNodeParent(node_id=self.peer_id, node_kind=self.peer_kind)
elif self.peer_id_previous and self.peer_kind_previous:
self._parent = ChangelogNodeParent(node_id=self.peer_id_previous, node_kind=self.peer_kind_previous)

@property
def is_empty(self) -> bool:
return self.peer_status == DiffAction.UNCHANGED and not self.properties
Expand Down Expand Up @@ -179,6 +198,11 @@ def is_empty(self) -> bool:
return not self.peers


class ChangelogNodeParent(BaseModel):
node_id: str
node_kind: str


class NodeChangelog(BaseModel):
"""Emitted when a node is updated"""

Expand All @@ -191,12 +215,35 @@ class NodeChangelog(BaseModel):
default_factory=dict
)

_parent: ChangelogNodeParent | None = PrivateAttr(default=None)

@property
def parent(self) -> ChangelogNodeParent | None:
return self._parent

@property
def root_node_id(self) -> str:
"""Return the top level node_id"""
if self.parent:
return self.parent.node_id
return self.node_id

def add_parent(self, parent: ChangelogNodeParent) -> None:
self._parent = parent

def add_parent_from_relationship(self, parent: Relationship) -> None:
self._parent = ChangelogNodeParent(node_id=parent.get_peer_id(), node_kind=parent.get_peer_kind())

def create_relationship(self, relationship: Relationship) -> None:
if relationship.schema.cardinality == RelationshipCardinality.ONE:
peer_id = relationship.get_peer_id()
peer_kind = relationship.get_peer_kind()
if relationship.schema.kind == RelationshipKind.PARENT:
self._parent = ChangelogNodeParent(node_id=peer_id, node_kind=peer_kind)
changelog_relationship = RelationshipCardinalityOneChangelog(
name=relationship.schema.name,
peer_id=relationship.get_peer_id(),
peer_kind=relationship.get_peer_kind(),
peer_id=peer_id,
peer_kind=peer_kind,
)
if source_id := getattr(relationship, "source_id", None):
changelog_relationship.add_property(name="source", value_current=source_id, value_previous=None)
Expand Down Expand Up @@ -226,6 +273,8 @@ def add_attribute(self, attribute: AttributeChangelog) -> None:
def add_relationship(
self, relationship: RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog
) -> None:
if isinstance(relationship, RelationshipCardinalityOneChangelog) and relationship.parent:
self.add_parent(parent=relationship.parent)
if relationship.is_empty:
return

Expand Down Expand Up @@ -276,6 +325,13 @@ def remove_peer(self, peer_data: RelationshipPeerData) -> None:
def _set_cardinality_one_peer(self, relationship: Relationship) -> None:
self.cardinality_one_relationship.peer_id = relationship.peer_id
self.cardinality_one_relationship.peer_kind = relationship.get_peer_kind()
self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)

def add_parent_from_relationship(self, relationship: Relationship) -> None:
if self.schema.cardinality == RelationshipCardinality.ONE:
self.cardinality_one_relationship.set_parent(
parent_id=relationship.get_peer_id(), parent_kind=relationship.get_peer_kind()
)

def add_peer_from_relationship(self, relationship: Relationship) -> None:
if self.schema.cardinality == RelationshipCardinality.ONE:
Expand Down Expand Up @@ -306,11 +362,14 @@ def add_updated_relationship(
value_current=getattr(relationship, property_name),
value_previous=previous_value,
)
self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)

def delete_relationship(self, relationship: Relationship) -> None:
if self.schema.cardinality == RelationshipCardinality.ONE:
self.cardinality_one_relationship.peer_id_previous = relationship.get_peer_id()
self.cardinality_one_relationship.peer_kind_previous = relationship.get_peer_kind()
self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)

elif self.schema.cardinality == RelationshipCardinality.MANY:
self.cardinality_many_relationship.remove_peer(
peer_id=relationship.get_peer_id(), peer_kind=relationship.get_peer_kind()
Expand Down
24 changes: 23 additions & 1 deletion backend/infrahub/core/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@

from infrahub.core import registry
from infrahub.core.changelog.models import NodeChangelog
from infrahub.core.constants import BranchSupportType, ComputedAttributeKind, InfrahubKind, RelationshipCardinality
from infrahub.core.constants import (
BranchSupportType,
ComputedAttributeKind,
InfrahubKind,
RelationshipCardinality,
RelationshipKind,
)
from infrahub.core.constants.schema import SchemaElementPathType
from infrahub.core.protocols import CoreNumberPool
from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
Expand Down Expand Up @@ -581,12 +587,22 @@ async def _update(
node_changelog.add_attribute(attribute=updated_attribute)

# Go over the list of relationships and update them one by one
processed_relationships: list[str] = []
for name in self._relationships:
if (fields and name in fields) or not fields:
processed_relationships.append(name)
rel: RelationshipManager = getattr(self, name)
updated_relationship = await rel.save(at=update_at, db=db)
node_changelog.add_relationship(relationship=updated_relationship)

if len(processed_relationships) != len(self._relationships):
# Analyze if the node has a parent and add it to the changelog if missing
if parent_relationship := self._get_parent_relationship_name():
if parent_relationship not in processed_relationships:
rel: RelationshipManager = getattr(self, parent_relationship)
for peer in await rel.get_relationships(db=db):
node_changelog.add_parent_from_relationship(parent=peer)

node_changelog.display_label = await self.render_display_label(db=db)
return node_changelog

Expand Down Expand Up @@ -782,3 +798,9 @@ async def render_display_label(self, db: Optional[InfrahubDatabase] = None) -> s
if not display_label.strip():
return repr(self)
return display_label.strip()

def _get_parent_relationship_name(self) -> str | None:
"""Return the name of the parent relationship is one is present"""
for relationship in self._schema.relationships:
if relationship.kind == RelationshipKind.PARENT:
return relationship.name
4 changes: 3 additions & 1 deletion backend/infrahub/core/relationship/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from infrahub.core import registry
from infrahub.core.changelog.models import ChangelogRelationshipMapper
from infrahub.core.constants import BranchSupportType, InfrahubKind
from infrahub.core.constants import BranchSupportType, InfrahubKind, RelationshipKind
from infrahub.core.property import (
FlagPropertyMixin,
NodePropertyData,
Expand Down Expand Up @@ -1179,6 +1179,8 @@ async def save(
old_data=details.peers_database[rel.peer_id],
properties_to_update=properties_not_matching,
)
elif rel.schema.kind == RelationshipKind.PARENT:
relationship_mapper.add_parent_from_relationship(relationship=rel)

return relationship_mapper.changelog

Expand Down
10 changes: 10 additions & 0 deletions backend/infrahub/events/node_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ def get_related(self) -> list[dict[str, str]]:
"infrahub.attribute.action": attribute.value_update_status.value, # type: ignore[attr-defined]
}
)
if self.data.parent:
related.append(
{
"prefect.resource.id": self.data.parent.node_id,
"prefect.resource.role": "infrahub.node.parent",
"infrahub.parent.kind": self.data.parent.node_kind,
"infrahub.parent.id": self.data.parent.node_id,
}
)

return related

Expand All @@ -49,6 +58,7 @@ def get_resource(self) -> dict[str, str]:
"infrahub.node.kind": self.kind,
"infrahub.node.id": self.node_id,
"infrahub.node.action": self.action.value,
"infrahub.node.root_id": self.data.root_node_id,
}

def get_payload(self) -> dict[str, Any]:
Expand Down
62 changes: 62 additions & 0 deletions backend/tests/unit/core/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ async def test_node_changelog_creation(db: InfrahubDatabase, default_branch, ani
},
relationships=owner,
)
assert not dog1.node_changelog.parent


async def test_node_changelog_update_with_cardinality_one_relationship(
Expand Down Expand Up @@ -182,6 +183,7 @@ async def test_node_changelog_update_with_cardinality_one_relationship(
)
},
)
assert not dog1_update.node_changelog.parent


async def test_node_changelog_update_with_cardinality_many_relationship(
Expand Down Expand Up @@ -228,6 +230,7 @@ async def test_node_changelog_update_with_cardinality_many_relationship(
)
in group1.node_changelog.relationships["members"].peers
)
assert not group1.node_changelog.parent


async def test_node_changelog_delete_with_cardinality_one_relationship(
Expand All @@ -249,6 +252,7 @@ async def test_node_changelog_delete_with_cardinality_one_relationship(
assert dog1_update.node_changelog.attributes["breed"].value_update_status == DiffAction.REMOVED
assert list(dog1_update.node_changelog.relationships.keys()) == ["owner"]
assert dog1_update.node_changelog.relationships["owner"].peer_status == DiffAction.REMOVED
assert not dog1_update.node_changelog.parent


async def test_node_changelog_delete_with_cardinality_many_relationship(
Expand All @@ -268,10 +272,68 @@ async def test_node_changelog_delete_with_cardinality_many_relationship(
dog2 = await Node.init(db=db, schema=dog_schema, branch=default_branch)
await dog2.new(db=db, name={"value": "Lassie", "owner": person1.id}, breed="Collie", owner=person1)
await dog2.save(db=db)
assert not dog2.node_changelog.parent

person1_update = await NodeManager.get_one(id=person1.id, db=db)
await person1_update.delete(db=db)

animals = person1_update.node_changelog.relationships["animals"].peers
assert RelationshipPeerChangelog(peer_id=dog1.id, peer_kind="TestAnimal", peer_status=DiffAction.REMOVED) in animals
assert RelationshipPeerChangelog(peer_id=dog2.id, peer_kind="TestAnimal", peer_status=DiffAction.REMOVED) in animals


async def test_node_changelog_parent(db: InfrahubDatabase, default_branch, car_person_schema: None) -> None:
"""Validate that parents are registrered in the node_changelog for parent/component relationships."""
person1 = await Node.init(db=db, schema="TestPerson", branch=default_branch)
await person1.new(db=db, name="Jack")
await person1.save(db=db)

person2 = await Node.init(db=db, schema="TestPerson", branch=default_branch)
await person2.new(db=db, name="Jill")
await person2.save(db=db)

# Person 1 should be identified as the parent on creation
car1 = await Node.init(db=db, schema="TestCar", branch=default_branch)
await car1.new(db=db, name="Volvo", owner=person1)
await car1.save(db=db)
assert car1.node_changelog.parent
assert car1.node_changelog.parent.node_id == person1.id
assert car1.node_changelog.parent.node_kind == "TestPerson"

# Person 1 should be identified as the parent on update even though the relationship wasn't modified
car1_update1 = await NodeManager.get_one(id=car1.id, db=db)
car1_update1.color.value = "Blue"
await car1_update1.save(db=db)
assert sorted(car1_update1.node_changelog.attributes.keys()) == ["color"]
assert not car1_update1.node_changelog.relationships
assert car1_update1.node_changelog.parent
assert car1_update1.node_changelog.parent.node_id == person1.id
assert car1_update1.node_changelog.parent.node_kind == "TestPerson"

# Person 1 should be identified as the parent on update even though the relationship wasn't modified and the .save()
# method was called with a fields filter
car1_update2 = await NodeManager.get_one(id=car1.id, db=db)
car1_update2.nbr_seats.value = 5
await car1_update2.save(db=db, fields=["nbr_seats"])
assert sorted(car1_update2.node_changelog.attributes.keys()) == ["nbr_seats"]
assert not car1_update2.node_changelog.relationships
assert car1_update2.node_changelog.parent
assert car1_update2.node_changelog.parent.node_id == person1.id
assert car1_update2.node_changelog.parent.node_kind == "TestPerson"

# Person 2 should be identified as the parent on update even after the owner is changed
# method was called with a fields filter
car1_update3 = await NodeManager.get_one(id=car1.id, db=db)
await car1_update3.owner.update(data=person2, db=db)
await car1_update3.save(db=db, fields=["owner"])
assert not car1_update3.node_changelog.attributes
assert sorted(car1_update3.node_changelog.relationships.keys()) == ["owner"]
assert car1_update3.node_changelog.parent
assert car1_update3.node_changelog.parent.node_id == person2.id
assert car1_update3.node_changelog.parent.node_kind == "TestPerson"

# Person 2 is still identified as the owner when the node is deleted
car1_delete1 = await NodeManager.get_one(id=car1.id, db=db)
await car1_delete1.delete(db=db)
assert car1_delete1.node_changelog.parent.node_id == person2.id
assert car1_delete1.node_changelog.parent.node_kind == "TestPerson"

0 comments on commit f4baa7a

Please sign in to comment.