diff --git a/backend/infrahub/core/diff/combiner.py b/backend/infrahub/core/diff/combiner.py index 986af352b6..ec4caddf6d 100644 --- a/backend/infrahub/core/diff/combiner.py +++ b/backend/infrahub/core/diff/combiner.py @@ -3,7 +3,6 @@ from typing import Iterable from uuid import uuid4 -from infrahub.core import registry from infrahub.core.constants import DiffAction, RelationshipCardinality from .model.path import ( @@ -25,7 +24,6 @@ class NodePair: class DiffCombiner: def __init__(self) -> None: - self.schema_manager = registry.schema # {child_uuid: (parent_uuid, parent_rel_name)} self._child_parent_uuid_map: dict[str, tuple[str, str]] = {} self._parent_node_uuids: set[str] = set() @@ -244,9 +242,7 @@ def _combine_relationships( self, earlier_relationships: set[EnrichedDiffRelationship], later_relationships: set[EnrichedDiffRelationship], - node_kind: str, ) -> set[EnrichedDiffRelationship]: - node_schema = self.schema_manager.get_node_schema(name=node_kind, branch=self.diff_branch_name, duplicate=False) earlier_rels_by_name = {rel.name: rel for rel in earlier_relationships} later_rels_by_name = {rel.name: rel for rel in later_relationships} common_rel_names = set(earlier_rels_by_name.keys()) & set(later_rels_by_name.keys()) @@ -257,12 +253,10 @@ def _combine_relationships( copied.nodes = set() combined_relationships.add(copied) continue - relationship_schema = node_schema.get_relationship(name=earlier_relationship.name) - is_cardinality_one = relationship_schema.cardinality is RelationshipCardinality.ONE later_relationship = later_rels_by_name[earlier_relationship.name] if len(earlier_relationship.relationships) == 0 and len(later_relationship.relationships) == 0: combined_relationship_elements = set() - elif is_cardinality_one: + elif earlier_relationship.cardinality is RelationshipCardinality.ONE: combined_relationship_elements = { self._combine_cardinality_one_relationship_elements( elements=(earlier_relationship.relationships | later_relationship.relationships) @@ -275,6 +269,7 @@ def _combine_relationships( combined_relationship = EnrichedDiffRelationship( name=later_relationship.name, label=later_relationship.label, + cardinality=later_relationship.cardinality, changed_at=later_relationship.changed_at or earlier_relationship.changed_at, action=self._combine_actions(earlier=earlier_relationship.action, later=later_relationship.action), path_identifier=later_relationship.path_identifier, @@ -315,7 +310,6 @@ def _combine_nodes(self, node_pairs: list[NodePair]) -> set[EnrichedDiffNode]: combined_relationships = self._combine_relationships( earlier_relationships=node_pair.earlier.relationships, later_relationships=node_pair.later.relationships, - node_kind=node_pair.later.kind, ) combined_action = self._combine_actions(earlier=node_pair.earlier.action, later=node_pair.later.action) combined_nodes.add( diff --git a/backend/infrahub/core/diff/enricher/hierarchy.py b/backend/infrahub/core/diff/enricher/hierarchy.py index af3523638f..79c6ba44c0 100644 --- a/backend/infrahub/core/diff/enricher/hierarchy.py +++ b/backend/infrahub/core/diff/enricher/hierarchy.py @@ -92,6 +92,7 @@ async def _enrich_hierarchical_nodes( parent_kind=ancestor.kind, parent_label="", parent_rel_name=parent_rel.name, + parent_rel_cardinality=parent_rel.cardinality, parent_rel_label=parent_rel.label or "", ) @@ -149,6 +150,7 @@ async def _enrich_nodes_with_parent( parent_kind=peer_parent.peer_kind, parent_label="", parent_rel_name=parent_rel.name, + parent_rel_cardinality=parent_rel.cardinality, parent_rel_label=parent_rel.label or "", ) diff --git a/backend/infrahub/core/diff/model/path.py b/backend/infrahub/core/diff/model/path.py index 1c075faba5..19e4ba91da 100644 --- a/backend/infrahub/core/diff/model/path.py +++ b/backend/infrahub/core/diff/model/path.py @@ -2,10 +2,10 @@ from dataclasses import dataclass, field, replace from enum import Enum -from typing import TYPE_CHECKING, Any, Optional, Self +from typing import TYPE_CHECKING, Any, Optional from uuid import uuid4 -from infrahub.core.constants import DiffAction, RelationshipDirection, RelationshipStatus +from infrahub.core.constants import DiffAction, RelationshipCardinality, RelationshipDirection, RelationshipStatus from infrahub.core.constants.database import DatabaseEdgeType from infrahub.core.timestamp import Timestamp @@ -193,6 +193,7 @@ def from_calculated_element(cls, calculated_element: DiffSingleRelationship) -> class EnrichedDiffRelationship(BaseSummary): name: str label: str + cardinality: RelationshipCardinality path_identifier: str = field(default="", kw_only=True) changed_at: Timestamp | None = field(default=None, kw_only=True) action: DiffAction @@ -219,6 +220,7 @@ def from_calculated_relationship(cls, calculated_relationship: DiffRelationship) return EnrichedDiffRelationship( name=calculated_relationship.name, label="", + cardinality=calculated_relationship.cardinality, changed_at=calculated_relationship.changed_at, action=calculated_relationship.action, relationships={ @@ -228,22 +230,6 @@ def from_calculated_relationship(cls, calculated_relationship: DiffRelationship) nodes=set(), ) - @classmethod - def from_graph(cls, node: Neo4jNode) -> Self: - timestamp_str = node.get("changed_at") - return cls( - name=node.get("name"), - label=node.get("label"), - changed_at=Timestamp(timestamp_str) if timestamp_str else None, - action=DiffAction(str(node.get("action"))), - path_identifier=str(node.get("path_identifier")), - num_added=int(node.get("num_added")), - num_conflicts=int(node.get("num_conflicts")), - num_removed=int(node.get("num_removed")), - num_updated=int(node.get("num_updated")), - contains_conflict=str(node.get("contains_conflict")).lower() == "true", - ) - @dataclass class ParentNodeInfo: @@ -357,14 +343,6 @@ def from_calculated_node(cls, calculated_node: DiffNode) -> EnrichedDiffNode: }, ) - def add_relationship_from_DiffRelationship(self, diff_rel: Neo4jNode) -> bool: - if self.has_relationship(name=diff_rel.get("name")): - return False - - rel = EnrichedDiffRelationship.from_graph(node=diff_rel) - self.relationships.add(rel) - return True - @dataclass class EnrichedDiffRoot(BaseSummary): @@ -423,6 +401,7 @@ def add_parent( parent_kind: str, parent_label: str, parent_rel_name: str, + parent_rel_cardinality: RelationshipCardinality, parent_rel_label: str = "", ) -> EnrichedDiffNode: node = self.get_node(node_uuid=node_id) @@ -447,6 +426,7 @@ def add_parent( EnrichedDiffRelationship( name=parent_rel_name, label=parent_rel_label, + cardinality=parent_rel_cardinality, changed_at=None, action=DiffAction.UNCHANGED, nodes={parent}, @@ -516,6 +496,7 @@ class DiffSingleRelationship: @dataclass class DiffRelationship: name: str + cardinality: RelationshipCardinality changed_at: Timestamp action: DiffAction relationships: list[DiffSingleRelationship] = field(default_factory=list) diff --git a/backend/infrahub/core/diff/query/save_query.py b/backend/infrahub/core/diff/query/save_query.py index ca2ea59ead..314192fb0a 100644 --- a/backend/infrahub/core/diff/query/save_query.py +++ b/backend/infrahub/core/diff/query/save_query.py @@ -197,6 +197,7 @@ def _build_diff_relationship_params(self, enriched_relationship: EnrichedDiffRel "node_properties": { "name": enriched_relationship.name, "label": enriched_relationship.label, + "cardinality": enriched_relationship.cardinality.value, "changed_at": enriched_relationship.changed_at.to_string() if enriched_relationship.changed_at else None, diff --git a/backend/infrahub/core/diff/query_parser.py b/backend/infrahub/core/diff/query_parser.py index 35061ce9bf..82e908d077 100644 --- a/backend/infrahub/core/diff/query_parser.py +++ b/backend/infrahub/core/diff/query_parser.py @@ -339,7 +339,11 @@ def to_diff_relationship(self, from_time: Timestamp) -> DiffRelationship: ): action = single_relationships[0].action return DiffRelationship( - name=self.name, changed_at=last_changed_at, action=action, relationships=single_relationships + name=self.name, + changed_at=last_changed_at, + action=action, + relationships=single_relationships, + cardinality=self.cardinality, ) diff --git a/backend/infrahub/core/diff/repository/deserializer.py b/backend/infrahub/core/diff/repository/deserializer.py index ff75c8198e..fd04f227dd 100644 --- a/backend/infrahub/core/diff/repository/deserializer.py +++ b/backend/infrahub/core/diff/repository/deserializer.py @@ -3,7 +3,7 @@ from neo4j.graph import Node as Neo4jNode from neo4j.graph import Path as Neo4jPath -from infrahub.core.constants import DiffAction +from infrahub.core.constants import DiffAction, RelationshipCardinality from infrahub.core.constants.database import DatabaseEdgeType from infrahub.core.query import QueryResult from infrahub.core.timestamp import Timestamp @@ -89,15 +89,14 @@ def _deserialize_relationships( ) -> None: for relationship_result in result.get_nested_node_collection("diff_relationships"): group_node, element_node, element_conflict, property_node, property_conflict = relationship_result - if group_node is not None and element_node is None and property_node is None: - enriched_node.add_relationship_from_DiffRelationship(diff_rel=group_node) - continue - if group_node is None or element_node is None or property_node is None: + enriched_relationship_group = None + if group_node: + enriched_relationship_group = self._deserialize_diff_relationship_group( + relationship_group_node=group_node, enriched_root=enriched_root, enriched_node=enriched_node + ) + if element_node is None or property_node is None or enriched_relationship_group is None: continue - enriched_relationship_group = self._deserialize_diff_relationship_group( - relationship_group_node=group_node, enriched_root=enriched_root, enriched_node=enriched_node - ) enriched_relationship_element = self._deserialize_diff_relationship_element( relationship_element_node=element_node, enriched_relationship_group=enriched_relationship_group, @@ -137,6 +136,7 @@ def _deserialize_parents(self, result: QueryResult, enriched_root: EnrichedDiffR parent_kind=parent.get("kind"), parent_label=parent.get("label"), parent_rel_name=rel.get("name"), + parent_rel_cardinality=RelationshipCardinality(rel.get("cardinality")), parent_rel_label=rel.get("label"), ) current_node_uuid = parent.get("uuid") @@ -232,7 +232,21 @@ def _deserialize_diff_relationship_group( if rel_key in self._diff_node_rel_group_map: return self._diff_node_rel_group_map[rel_key] - enriched_relationship = EnrichedDiffRelationship.from_graph(node=relationship_group_node) + timestamp_str = relationship_group_node.get("changed_at") + enriched_relationship = EnrichedDiffRelationship( + name=relationship_group_node.get("name"), + label=relationship_group_node.get("label"), + cardinality=RelationshipCardinality(relationship_group_node.get("cardinality")), + changed_at=Timestamp(timestamp_str) if timestamp_str else None, + action=DiffAction(str(relationship_group_node.get("action"))), + path_identifier=str(relationship_group_node.get("path_identifier")), + num_added=int(relationship_group_node.get("num_added")), + num_conflicts=int(relationship_group_node.get("num_conflicts")), + num_removed=int(relationship_group_node.get("num_removed")), + num_updated=int(relationship_group_node.get("num_updated")), + contains_conflict=str(relationship_group_node.get("contains_conflict")).lower() == "true", + ) + self._diff_node_rel_group_map[rel_key] = enriched_relationship enriched_node.relationships.add(enriched_relationship) return enriched_relationship diff --git a/backend/infrahub/graphql/queries/diff/tree.py b/backend/infrahub/graphql/queries/diff/tree.py index 4834634e2b..63a885c509 100644 --- a/backend/infrahub/graphql/queries/diff/tree.py +++ b/backend/infrahub/graphql/queries/diff/tree.py @@ -7,7 +7,7 @@ from infrahub_sdk.utils import extract_fields from infrahub.core import registry -from infrahub.core.constants import DiffAction +from infrahub.core.constants import DiffAction, RelationshipCardinality from infrahub.core.diff.model.path import NameTrackingId from infrahub.core.diff.repository.repository import DiffRepository from infrahub.core.timestamp import Timestamp @@ -31,6 +31,7 @@ from infrahub.graphql import GraphqlContext GrapheneDiffActionEnum = GrapheneEnum.from_enum(DiffAction) +GrapheneCardinalityEnum = GrapheneEnum.from_enum(RelationshipCardinality) class ConflictDetails(ObjectType): @@ -85,6 +86,7 @@ class DiffRelationship(DiffSummaryCounts): name = String(required=True) label = String(required=False) last_changed_at = DateTime(required=False) + cardinality = Field(GrapheneCardinalityEnum, required=True) status = Field(GrapheneDiffActionEnum, required=True) path_identifier = String(required=True) elements = List(DiffSingleRelationship, required=True) @@ -220,6 +222,7 @@ def to_diff_relationship( label=enriched_relationship.label, last_changed_at=enriched_relationship.changed_at.obj if enriched_relationship.changed_at else None, status=enriched_relationship.action, + cardinality=enriched_relationship.cardinality, path_identifier=enriched_relationship.path_identifier, elements=diff_elements, contains_conflict=enriched_relationship.contains_conflict, diff --git a/backend/tests/unit/core/diff/test_diff_combiner.py b/backend/tests/unit/core/diff/test_diff_combiner.py index 0dc72bf629..6f03873625 100644 --- a/backend/tests/unit/core/diff/test_diff_combiner.py +++ b/backend/tests/unit/core/diff/test_diff_combiner.py @@ -7,7 +7,7 @@ from pendulum.datetime import DateTime from infrahub.core import registry -from infrahub.core.constants import DiffAction +from infrahub.core.constants import DiffAction, RelationshipCardinality from infrahub.core.constants.database import DatabaseEdgeType from infrahub.core.diff.combiner import DiffCombiner from infrahub.core.diff.model.path import ( @@ -373,11 +373,16 @@ async def test_relationship_one_combined(self, with_schema_manager): conflict=EnrichedConflictFactory.build(), ) early_relationship = EnrichedRelationshipGroupFactory.build( - name=relationship_name, action=DiffAction.ADDED, relationships={early_element}, nodes=set() + name=relationship_name, + action=DiffAction.ADDED, + relationships={early_element}, + nodes=set(), + cardinality=RelationshipCardinality.ONE, ) later_relationship = EnrichedRelationshipGroupFactory.build( name=relationship_name, action=DiffAction.UPDATED, + cardinality=RelationshipCardinality.ONE, relationships={later_element}, nodes=set(), changed_at=Timestamp(), @@ -418,6 +423,7 @@ async def test_relationship_one_combined(self, with_schema_manager): expected_relationship = EnrichedDiffRelationship( name=relationship_name, label=later_relationship.label, + cardinality=RelationshipCardinality.ONE, changed_at=later_relationship.changed_at, action=DiffAction.ADDED, path_identifier=later_relationship.path_identifier, @@ -521,12 +527,14 @@ async def test_relationship_many_combined(self, with_schema_manager): relationship_group_1 = EnrichedRelationshipGroupFactory.build( name=relationship_name, action=DiffAction.UPDATED, + cardinality=RelationshipCardinality.MANY, relationships={added_element_1, removed_element_1, updated_element_1, canceled_element_1}, nodes=set(), ) relationship_group_2 = EnrichedRelationshipGroupFactory.build( name=relationship_name, action=DiffAction.UPDATED, + cardinality=RelationshipCardinality.MANY, relationships={added_element_2, removed_element_2, updated_element_2, canceled_element_2}, changed_at=Timestamp(), nodes=set(), @@ -580,6 +588,7 @@ async def test_relationship_many_combined(self, with_schema_manager): expected_relationship = EnrichedDiffRelationship( name=relationship_name, label=relationship_group_2.label, + cardinality=RelationshipCardinality.MANY, changed_at=relationship_group_2.changed_at, action=DiffAction.UPDATED, path_identifier=relationship_group_2.path_identifier, @@ -609,7 +618,11 @@ async def test_relationship_with_only_nodes(self, with_schema_manager): action=DiffAction.UNCHANGED, relationships=set(), attributes=set() ) early_relationship = EnrichedRelationshipGroupFactory.build( - name=relationship_name, action=DiffAction.ADDED, relationships=set(), nodes={early_parent_node} + name=relationship_name, + action=DiffAction.ADDED, + cardinality=RelationshipCardinality.MANY, + relationships=set(), + nodes={early_parent_node}, ) later_parent_node = EnrichedNodeFactory.build( action=DiffAction.UNCHANGED, relationships=set(), attributes=set() @@ -617,6 +630,7 @@ async def test_relationship_with_only_nodes(self, with_schema_manager): later_relationship = EnrichedRelationshipGroupFactory.build( name=relationship_name, action=DiffAction.UPDATED, + cardinality=RelationshipCardinality.MANY, relationships=set(), nodes={later_parent_node}, changed_at=Timestamp(), @@ -637,6 +651,7 @@ async def test_relationship_with_only_nodes(self, with_schema_manager): expected_relationship = EnrichedDiffRelationship( name=relationship_name, label=later_relationship.label, + cardinality=RelationshipCardinality.MANY, changed_at=later_relationship.changed_at, action=DiffAction.ADDED, path_identifier=later_relationship.path_identifier, @@ -768,10 +783,15 @@ async def test_unchanged_parents_correctly_updated(self): action=DiffAction.UNCHANGED, attributes=set(), relationships=set(), changed_at=Timestamp() ) parent_rel_1 = EnrichedRelationshipGroupFactory.build( - name=relationship_name, relationships=set(), nodes={parent_node_1}, action=DiffAction.UNCHANGED + name=relationship_name, + relationships=set(), + nodes={parent_node_1}, + action=DiffAction.UNCHANGED, + cardinality=RelationshipCardinality.ONE, ) parent_rel_2 = EnrichedRelationshipGroupFactory.build( name=relationship_name, + cardinality=RelationshipCardinality.ONE, relationships=set(), nodes={parent_node_2}, action=DiffAction.UNCHANGED, @@ -797,6 +817,7 @@ async def test_unchanged_parents_correctly_updated(self): name=relationship_name, label=parent_rel_2.label, changed_at=parent_rel_2.changed_at, + cardinality=RelationshipCardinality.ONE, path_identifier=parent_rel_2.path_identifier, action=DiffAction.UNCHANGED, relationships=set(), @@ -832,6 +853,7 @@ async def test_updated_parents_correctly_updated(self): name=relationship_name, relationships={child_element_1}, nodes={parent_node_1}, + cardinality=RelationshipCardinality.ONE, action=DiffAction.UPDATED, num_added=0, num_updated=0, @@ -843,6 +865,7 @@ async def test_updated_parents_correctly_updated(self): name=relationship_name, relationships=set(), nodes={parent_node_2}, + cardinality=RelationshipCardinality.ONE, action=DiffAction.UNCHANGED, changed_at=Timestamp(), ) @@ -863,6 +886,7 @@ async def test_updated_parents_correctly_updated(self): name=relationship_name, label=child_rel_2.label, changed_at=child_rel_2.changed_at, + cardinality=RelationshipCardinality.ONE, path_identifier=child_rel_2.path_identifier, action=DiffAction.UPDATED, relationships={child_element_1}, diff --git a/backend/tests/unit/graphql/test_diff_tree_query.py b/backend/tests/unit/graphql/test_diff_tree_query.py index 816276b917..bf8349ae4e 100644 --- a/backend/tests/unit/graphql/test_diff_tree_query.py +++ b/backend/tests/unit/graphql/test_diff_tree_query.py @@ -14,6 +14,7 @@ from infrahub.core.manager import NodeManager from infrahub.core.node import Node from infrahub.core.schema import NodeSchema +from infrahub.core.schema_manager import SchemaBranch from infrahub.database import InfrahubDatabase from infrahub.dependencies.registry import get_component_registry from infrahub.graphql import prepare_graphql_params @@ -23,6 +24,11 @@ UPDATED_ACTION = "UPDATED" REMOVED_ACTION = "REMOVED" UNCHANGED_ACTION = "UNCHANGED" +CARDINALITY_ONE = "ONE" +CARDINALITY_MANY = "MANY" +IS_RELATED_TYPE = "IS_RELATED" +IS_PROTECTED_TYPE = "IS_PROTECTED" +IS_VISIBLE_TYPE = "IS_VISIBLE" DIFF_TREE_QUERY = """ query GetDiffTree($branch: String){ @@ -82,6 +88,7 @@ name last_changed_at status + cardinality contains_conflict elements { status @@ -315,6 +322,236 @@ async def test_diff_tree_one_attr_change( } +async def test_diff_tree_one_relationship_change( + db: InfrahubDatabase, + default_branch: Branch, + car_person_schema: SchemaBranch, + car_accord_main: Node, + person_john_main: Node, + person_jane_main: Node, + diff_branch: Branch, + diff_coordinator: DiffCoordinator, + diff_repository: DiffRepository, +): + branch_car = await NodeManager.get_one(db=db, id=car_accord_main.id, branch=diff_branch) + await branch_car.owner.update(db=db, data=[person_jane_main]) + before_change_datetime = datetime.now(tz=UTC) + await branch_car.save(db=db) + after_change_datetime = datetime.now(tz=UTC) + + enriched_diff = await diff_coordinator.update_branch_diff(base_branch=default_branch, diff_branch=diff_branch) + params = prepare_graphql_params(db=db, include_mutation=False, include_subscription=False, branch=default_branch) + result = await graphql( + schema=params.schema, + source=DIFF_TREE_QUERY, + context_value=params.context, + root_value=None, + variable_values={"branch": diff_branch.name}, + ) + from_time = datetime.fromisoformat(diff_branch.created_at) + to_time = datetime.fromisoformat(enriched_diff.to_time.to_string()) + + assert result.errors is None + + assert result.data["DiffTree"] + diff_tree_response = result.data["DiffTree"].copy() + nodes_response = diff_tree_response.pop("nodes") + assert diff_tree_response == { + "base_branch": "main", + "diff_branch": diff_branch.name, + "from_time": from_time.isoformat(), + "to_time": to_time.isoformat(), + "num_added": 0, + "num_removed": 0, + "num_updated": 3, + "num_conflicts": 0, + } + assert len(nodes_response) == 3 + node_response_by_id = {n["uuid"]: n for n in nodes_response} + assert set(node_response_by_id.keys()) == {car_accord_main.id, person_john_main.id, person_jane_main.id} + # car node + car_response = node_response_by_id[car_accord_main.id] + car_relationship_response = car_response.pop("relationships") + car_changed_at = car_response["last_changed_at"] + assert datetime.fromisoformat(car_changed_at) < before_change_datetime + assert car_response == { + "uuid": car_accord_main.id, + "kind": car_accord_main.get_kind(), + "label": await car_accord_main.render_display_label(db=db), + "last_changed_at": car_changed_at, + "num_added": 0, + "num_removed": 0, + "num_updated": 1, + "num_conflicts": 0, + "parent": {"kind": person_jane_main.get_kind(), "relationship_name": "cars", "uuid": person_jane_main.get_id()}, + "status": UPDATED_ACTION, + "contains_conflict": False, + "attributes": [], + } + car_relationships_by_name = {r["name"]: r for r in car_relationship_response} + assert set(car_relationships_by_name.keys()) == {"owner"} + owner_rel = car_relationships_by_name["owner"] + owner_changed_at = owner_rel["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(owner_changed_at) < after_change_datetime + owner_elements = owner_rel.pop("elements") + assert owner_rel == { + "name": "owner", + "last_changed_at": owner_changed_at, + "status": UPDATED_ACTION, + "cardinality": "ONE", + "contains_conflict": False, + } + assert len(owner_elements) == 1 + owner_element = owner_elements[0] + owner_element_changed_at = owner_element["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(owner_element_changed_at) < after_change_datetime + owner_properties = owner_element.pop("properties") + assert owner_element == { + "status": UPDATED_ACTION, + "peer_id": person_jane_main.id, + "last_changed_at": owner_element_changed_at, + "contains_conflict": False, + "conflict": None, + } + owner_properties_by_type = {p["property_type"]: p for p in owner_properties} + assert set(owner_properties_by_type.keys()) == {IS_RELATED_TYPE} + owner_prop = owner_properties_by_type[IS_RELATED_TYPE] + owner_prop_changed_at = owner_prop["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(owner_prop_changed_at) < after_change_datetime + assert owner_prop == { + "property_type": IS_RELATED_TYPE, + "last_changed_at": owner_prop_changed_at, + "previous_value": person_john_main.id, + "new_value": person_jane_main.id, + "status": UPDATED_ACTION, + "conflict": None, + } + # john node + john_response = node_response_by_id[person_john_main.id] + john_relationship_response = john_response.pop("relationships") + john_changed_at = john_response["last_changed_at"] + assert datetime.fromisoformat(john_changed_at) < before_change_datetime + assert john_response == { + "uuid": person_john_main.id, + "kind": person_john_main.get_kind(), + "label": await person_john_main.render_display_label(db=db), + "last_changed_at": john_changed_at, + "num_added": 0, + "num_removed": 0, + "num_updated": 1, + "num_conflicts": 0, + "parent": None, + "status": UPDATED_ACTION, + "contains_conflict": False, + "attributes": [], + } + john_relationships_by_name = {r["name"]: r for r in john_relationship_response} + assert set(john_relationships_by_name.keys()) == {"cars"} + cars_rel = john_relationships_by_name["cars"] + cars_changed_at = cars_rel["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(cars_changed_at) < after_change_datetime + cars_elements = cars_rel.pop("elements") + assert cars_rel == { + "name": "cars", + "last_changed_at": cars_changed_at, + "status": UPDATED_ACTION, + "cardinality": "MANY", + "contains_conflict": False, + } + assert len(cars_elements) == 1 + cars_element = cars_elements[0] + cars_element_changed_at = cars_element["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(cars_element_changed_at) < after_change_datetime + cars_properties = cars_element.pop("properties") + assert cars_element == { + "status": REMOVED_ACTION, + "peer_id": car_accord_main.id, + "last_changed_at": cars_element_changed_at, + "contains_conflict": False, + "conflict": None, + } + cars_properties_by_type = {p["property_type"]: p for p in cars_properties} + assert set(cars_properties_by_type.keys()) == {IS_RELATED_TYPE, IS_VISIBLE_TYPE, IS_PROTECTED_TYPE} + for property_type, previous_value in [ + (IS_RELATED_TYPE, car_accord_main.id), + (IS_PROTECTED_TYPE, "False"), + (IS_VISIBLE_TYPE, "True"), + ]: + cars_prop = cars_properties_by_type[property_type] + cars_prop_changed_at = cars_prop["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(cars_prop_changed_at) < after_change_datetime + assert cars_prop == { + "property_type": property_type, + "last_changed_at": cars_prop_changed_at, + "previous_value": previous_value, + "new_value": None, + "status": REMOVED_ACTION, + "conflict": None, + } + # jane node + jane_response = node_response_by_id[person_jane_main.id] + jane_relationship_response = jane_response.pop("relationships") + jane_changed_at = jane_response["last_changed_at"] + assert datetime.fromisoformat(jane_changed_at) < before_change_datetime + assert jane_response == { + "uuid": person_jane_main.id, + "kind": person_jane_main.get_kind(), + "label": await person_jane_main.render_display_label(db=db), + "last_changed_at": jane_changed_at, + "num_added": 0, + "num_removed": 0, + "num_updated": 1, + "num_conflicts": 0, + "parent": None, + "status": UPDATED_ACTION, + "contains_conflict": False, + "attributes": [], + } + jane_relationships_by_name = {r["name"]: r for r in jane_relationship_response} + assert set(jane_relationships_by_name.keys()) == {"cars"} + cars_rel = jane_relationships_by_name["cars"] + cars_changed_at = cars_rel["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(cars_changed_at) < after_change_datetime + cars_elements = cars_rel.pop("elements") + assert cars_rel == { + "name": "cars", + "last_changed_at": cars_changed_at, + "status": UPDATED_ACTION, + "cardinality": "MANY", + "contains_conflict": False, + } + assert len(cars_elements) == 1 + cars_element = cars_elements[0] + cars_element_changed_at = cars_element["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(cars_element_changed_at) < after_change_datetime + cars_properties = cars_element.pop("properties") + assert cars_element == { + "status": ADDED_ACTION, + "peer_id": car_accord_main.id, + "last_changed_at": cars_element_changed_at, + "contains_conflict": False, + "conflict": None, + } + cars_properties_by_type = {p["property_type"]: p for p in cars_properties} + assert set(cars_properties_by_type.keys()) == {IS_RELATED_TYPE, IS_VISIBLE_TYPE, IS_PROTECTED_TYPE} + for property_type, new_value in [ + (IS_RELATED_TYPE, car_accord_main.id), + (IS_PROTECTED_TYPE, "False"), + (IS_VISIBLE_TYPE, "True"), + ]: + cars_prop = cars_properties_by_type[property_type] + cars_prop_changed_at = cars_prop["last_changed_at"] + assert before_change_datetime < datetime.fromisoformat(cars_prop_changed_at) < after_change_datetime + assert cars_prop == { + "property_type": property_type, + "last_changed_at": cars_prop_changed_at, + "previous_value": None, + "new_value": new_value, + "status": ADDED_ACTION, + "conflict": None, + } + + async def test_diff_tree_hierarchy_change( db: InfrahubDatabase, default_branch: Branch,