From 91e31f7fb81ac89f490a40b1dbaf2dfe98121e5f Mon Sep 17 00:00:00 2001 From: Matthias Veit Date: Wed, 13 Dec 2023 16:18:45 +0100 Subject: [PATCH] [resoto][fix] Change node should also reflect resolved properties (#1858) --- resotocore/resotocore/db/graphdb.py | 40 ++++++++++++++++++- .../resotocore/model/resolve_in_graph.py | 11 +++++ .../tests/resotocore/db/graphdb_test.py | 38 ++++++++++++------ 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/resotocore/resotocore/db/graphdb.py b/resotocore/resotocore/db/graphdb.py index 789546534..c913bd72b 100644 --- a/resotocore/resotocore/db/graphdb.py +++ b/resotocore/resotocore/db/graphdb.py @@ -7,6 +7,7 @@ from enum import Enum from functools import partial from numbers import Number +from textwrap import dedent from typing import ( DefaultDict, Optional, @@ -52,7 +53,7 @@ UsageDatapoint, synthetic_metadata_kinds, ) -from resotocore.model.resolve_in_graph import NodePath, GraphResolver +from resotocore.model.resolve_in_graph import NodePath, GraphResolver, ResolveProp from resotocore.model.typed_model import to_js from resotocore.query.model import Query, FulltextTerm, MergeTerm, P, Predicate from resotocore.report import ReportSeverity @@ -352,7 +353,27 @@ async def update_node_with( if sec in adjusted: update[sec] = adjusted[sec] - result = await db.update(self.vertex_name, update, return_new=True, merge=not replace) + async def update_resolved_property(id_prop: ResolveProp, patch: Json, history: bool) -> None: + log.info(f"Update resolved property: {id_prop.to}={patch} for node_id={node_id}") + async with await self.db.aql_cursor( + query=self.update_resolved(id_prop, history), bind_vars={"node_id": node_id, "patch": patch} + ) as crs: + async for el in crs: + part = self.node_history if history else self.vertex_name + log.info(f"Updated resolved property in {part}: {el} elements changed.") + + # update resolved properties in vertex and history collection + if (ra := GraphResolver.resolve_ancestor_for(update)) and (rid := ra.resolves_id()): + changes: Json = {} + for prop in ra.resolved_props(): + if value_in_path(node, prop.extract_path) != (nv := value_in_path(update, prop.extract_path)): + set_value_in_path(nv, prop.to_path, changes) + if changes: + await update_resolved_property(rid, changes, False) + await update_resolved_property(rid, changes, True) + + # update in database + result = await self.db.update(self.vertex_name, update, return_new=True, merge=not replace) trafo = self.document_to_instance_fn(model) return trafo(result["new"]) @@ -1464,6 +1485,21 @@ def update_active_change(self) -> str: UPDATE d WITH {{created: DATE_ISO8601(DATE_NOW())}} in `{self.in_progress}` """ # noqa: E501 + def update_resolved( + self, + prop: ResolveProp, + history: bool = False, + ) -> str: + coll = self.node_history if history else self.vertex_name + return dedent( + f""" + FOR d in `{coll}` FILTER d._key!=@node_id and d.{prop.to}==@node_id + UPDATE d WITH @patch in `{coll}` + COLLECT WITH COUNT INTO count + RETURN count + """ + ) + class EventGraphDB(GraphDB): def __init__(self, real: ArangoGraphDB, event_sender: AnalyticsEventSender): diff --git a/resotocore/resotocore/model/resolve_in_graph.py b/resotocore/resotocore/model/resolve_in_graph.py index 84c0c5cd9..4361493cc 100644 --- a/resotocore/resotocore/model/resolve_in_graph.py +++ b/resotocore/resotocore/model/resolve_in_graph.py @@ -56,6 +56,9 @@ class ResolveAncestor: # List of all properties to be resolved. resolve: List[ResolveProp] + def resolved_props(self) -> List[ResolveProp]: + return [prop for prop in self.resolve if prop.extract_path != NodePath.node_id] + def resolves_id(self) -> Optional[ResolveProp]: for prop in self.resolve: if prop.extract_path == NodePath.node_id: @@ -126,3 +129,11 @@ def resolved_kind(node: Json) -> Optional[str]: if kind in GraphResolver.resolved_ancestors: return kind return None + + @staticmethod + def resolve_ancestor_for(node: Json) -> Optional[ResolveAncestor]: + if kind := GraphResolver.resolved_kind(node): + for elem in GraphResolver.to_resolve: + if elem.kind == kind: + return elem + return None diff --git a/resotocore/tests/resotocore/db/graphdb_test.py b/resotocore/tests/resotocore/db/graphdb_test.py index 6c3f21bef..eab5a9c02 100644 --- a/resotocore/tests/resotocore/db/graphdb_test.py +++ b/resotocore/tests/resotocore/db/graphdb_test.py @@ -511,19 +511,31 @@ async def test_insert_node(graph_db: ArangoGraphDB, foo_model: Model) -> None: @mark.asyncio -async def test_update_node(graph_db: ArangoGraphDB, foo_model: Model) -> None: - await graph_db.wipe() - await graph_db.create_node(foo_model, NodeId("some_other"), to_json(Foo("some_other", "foo")), NodeId("root")) - json_patch = await graph_db.update_node(foo_model, NodeId("some_other"), {"name": "bla"}, False, "reported") - assert to_foo(json_patch).name == "bla" - assert to_foo(await graph_db.get_node(foo_model, NodeId("some_other"))).name == "bla" - json_replace = ( - await graph_db.update_node( - foo_model, NodeId("some_other"), {"kind": "bla", "identifier": "123"}, True, "reported" - ) - )["reported"] - json_replace.pop("ctime") # ctime is added by the system automatically. remove it - assert json_replace == {"kind": "bla", "identifier": "123"} +async def test_update_node(filled_graph_db: ArangoGraphDB, foo_model: Model) -> None: + nid = NodeId("0") + # patch + js = await filled_graph_db.update_node(foo_model, nid, {"name": "bla"}, False, "reported") + assert to_foo(js).name == "bla" + assert to_foo(await filled_graph_db.get_node(foo_model, nid)).name == "bla" + # replace + js = await filled_graph_db.update_node(foo_model, nid, {"kind": "bla", "identifier": "123"}, True, "reported") + reported = js["reported"] + reported.pop("ctime") # ctime is added by the system automatically. remove it + assert reported == {"kind": "bla", "identifier": "123"} + # also make sure that all resolved ancestor props are changed + nid = NodeId("sub_root") + # patch + js = await filled_graph_db.update_node(foo_model, nid, {"name": "bat"}, False, "reported") + assert js["reported"]["name"] == "bat" + + async def elements(history: bool) -> List[Json]: + fn = filled_graph_db.search_history if history else filled_graph_db.search_list + model = QueryModel(parse_query("ancestors.account.reported.name==bat"), foo_model) + async with await fn(query=model) as crs: # type: ignore + return [e async for e in crs] + + assert len(await elements(False)) == 110 + assert len(await elements(True)) == 111 # history includes the node itself @mark.asyncio