Skip to content

Commit 2843321

Browse files
authored
Merge pull request #5838 from opsmill/pog-node-changelog-cascade-delete-IFC-1275
Add node changelog events for cascade delete mutations
2 parents 6b333ec + 4c11cf2 commit 2843321

File tree

5 files changed

+121
-15
lines changed

5 files changed

+121
-15
lines changed

backend/infrahub/core/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,7 @@ async def delete(
13151315
nodes: list[Node],
13161316
branch: Optional[Union[Branch, str]] = None,
13171317
at: Optional[Union[Timestamp, str]] = None,
1318-
) -> list[Any]:
1318+
) -> list[Node]:
13191319
"""Returns list of deleted nodes because of cascading deletes"""
13201320
branch = await registry.get_branch(branch=branch, db=db)
13211321
node_delete_validator = NodeDeleteValidator(db=db, branch=branch)

backend/infrahub/graphql/mutations/ipam.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from infrahub.lock import InfrahubMultiLock, build_object_lock_name
2121
from infrahub.log import get_logger
2222

23-
from .main import InfrahubMutationMixin, InfrahubMutationOptions
23+
from .main import DeleteResult, InfrahubMutationMixin, InfrahubMutationOptions
2424

2525
if TYPE_CHECKING:
2626
from infrahub.graphql.initialization import GraphqlContext
@@ -235,7 +235,7 @@ async def mutate_delete(
235235
info: GraphQLResolveInfo,
236236
data: InputObjectType,
237237
branch: Branch,
238-
):
238+
) -> DeleteResult:
239239
return await super().mutate_delete(info=info, data=data, branch=branch)
240240

241241

@@ -402,7 +402,7 @@ async def mutate_delete(
402402
info: GraphQLResolveInfo,
403403
data: InputObjectType,
404404
branch: Branch,
405-
) -> tuple[Node, Self]:
405+
) -> DeleteResult:
406406
graphql_context: GraphqlContext = info.context
407407
db = graphql_context.db
408408

@@ -431,4 +431,4 @@ async def mutate_delete(
431431

432432
ok = True
433433

434-
return reconciled_prefix, cls(ok=ok)
434+
return DeleteResult(node=reconciled_prefix, mutation=cls(ok=ok))

backend/infrahub/graphql/mutations/main.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass, field
34
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union
45

56
from graphene import InputObjectType, Mutation
@@ -49,6 +50,13 @@
4950
KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [InfrahubKind.GENERICGROUP]
5051

5152

53+
@dataclass
54+
class DeleteResult:
55+
node: Node
56+
mutation: InfrahubMutationMixin
57+
deleted_nodes: list[Node] = field(default_factory=list)
58+
59+
5260
# ------------------------------------------
5361
# Infrahub GraphQLType
5462
# ------------------------------------------
@@ -64,6 +72,7 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: InputObjectTyp
6472
obj = None
6573
mutation = None
6674
action = MutationAction.UNDEFINED
75+
deleted_nodes: list[Node] = []
6776

6877
if "Create" in cls.__name__:
6978
obj, mutation = await cls.mutate_create(info=info, branch=graphql_context.branch, data=data, **kwargs)
@@ -86,7 +95,11 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: InputObjectTyp
8695
else:
8796
action = MutationAction.UPDATED
8897
elif "Delete" in cls.__name__:
89-
obj, mutation = await cls.mutate_delete(info=info, branch=graphql_context.branch, data=data, **kwargs)
98+
delete_result = await cls.mutate_delete(info=info, branch=graphql_context.branch, data=data, **kwargs)
99+
obj = delete_result.node
100+
mutation = delete_result.mutation
101+
deleted_nodes = delete_result.deleted_nodes
102+
90103
action = MutationAction.DELETED
91104
else:
92105
raise ValueError(
@@ -124,18 +137,34 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: InputObjectTyp
124137

125138
events = [main_event]
126139

127-
for node_changelog in node_changelogs:
140+
deleted_changelogs = [node.node_changelog for node in deleted_nodes if node.id != obj.id]
141+
deleted_ids = {node.node_id for node in deleted_changelogs}
142+
143+
for node_changelog in deleted_changelogs:
128144
meta = EventMeta.from_parent(parent=main_event)
129145
event = NodeMutatedEvent(
130146
kind=node_changelog.node_kind,
131147
node_id=node_changelog.node_id,
132148
data=node_changelog,
133-
action=MutationAction.UPDATED,
149+
action=MutationAction.DELETED,
134150
fields=node_changelog.updated_fields,
135151
meta=meta,
136152
)
137153
events.append(event)
138154

155+
for node_changelog in node_changelogs:
156+
if node_changelog.node_id not in deleted_ids:
157+
meta = EventMeta.from_parent(parent=main_event)
158+
event = NodeMutatedEvent(
159+
kind=node_changelog.node_kind,
160+
node_id=node_changelog.node_id,
161+
data=node_changelog,
162+
action=MutationAction.UPDATED,
163+
fields=node_changelog.updated_fields,
164+
meta=meta,
165+
)
166+
events.append(event)
167+
139168
for event in events:
140169
graphql_context.background.add_task(graphql_context.active_service.event.send, event)
141170

@@ -429,9 +458,9 @@ async def mutate_update_object(
429458
await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
430459

431460
fields = list(data.keys())
432-
for field in ("id", "hfid"):
433-
if field in fields:
434-
fields.remove(field)
461+
for field_to_remove in ("id", "hfid"):
462+
if field_to_remove in fields:
463+
fields.remove(field_to_remove)
435464

436465
await obj.save(db=db, fields=fields)
437466

@@ -494,7 +523,7 @@ async def mutate_delete(
494523
info: GraphQLResolveInfo,
495524
data: InputObjectType,
496525
branch: Branch,
497-
) -> tuple[Node, Self]:
526+
) -> DeleteResult:
498527
graphql_context: GraphqlContext = info.context
499528

500529
obj = await NodeManager.find_object(
@@ -512,7 +541,7 @@ async def mutate_delete(
512541

513542
ok = True
514543

515-
return obj, cls(ok=ok)
544+
return DeleteResult(node=obj, mutation=cls(ok=ok), deleted_nodes=deleted)
516545

517546

518547
class InfrahubMutation(InfrahubMutationMixin, Mutation):

backend/infrahub/graphql/mutations/menu.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from infrahub.exceptions import ValidationError
1515
from infrahub.graphql.mutations.main import InfrahubMutationMixin
1616

17-
from .main import InfrahubMutationOptions
17+
from .main import DeleteResult, InfrahubMutationOptions
1818

1919
if TYPE_CHECKING:
2020
from infrahub.graphql.initialization import GraphqlContext
@@ -89,7 +89,7 @@ async def mutate_delete(
8989
info: GraphQLResolveInfo,
9090
data: InputObjectType,
9191
branch: Branch,
92-
) -> tuple[Node, Self]:
92+
) -> DeleteResult:
9393
graphql_context: GraphqlContext = info.context
9494
obj = await NodeManager.find_object(
9595
db=graphql_context.db, kind=CoreMenuItem, id=data.get("id"), hfid=data.get("hfid"), branch=branch

backend/tests/unit/graphql/test_mutation_delete.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
from infrahub.auth import AccountSession
2+
from infrahub.core import registry
3+
from infrahub.core.branch import Branch
4+
from infrahub.core.constants import MutationAction, RelationshipDeleteBehavior
15
from infrahub.core.manager import NodeManager
26
from infrahub.core.node import Node
7+
from infrahub.core.schema.schema_branch import SchemaBranch
38
from infrahub.database import InfrahubDatabase
9+
from infrahub.events.models import ParentEvent
10+
from infrahub.events.node_action import NodeMutatedEvent
411
from infrahub.graphql.initialization import prepare_graphql_params
12+
from infrahub.services import InfrahubServices
13+
from tests.adapters.event import MemoryInfrahubEvent
514
from tests.helpers.graphql import graphql
615

716

@@ -36,6 +45,7 @@ async def test_delete_object(db: InfrahubDatabase, default_branch, car_person_sc
3645
)
3746

3847
assert result.errors is None
48+
assert result.data
3949
assert result.data["TestPersonDelete"]["ok"] is True
4050

4151
assert not await NodeManager.get_one(db=db, id=obj1.id)
@@ -70,6 +80,7 @@ async def test_delete_prevented(
7080
f"It is linked to mandatory relationship owner on node TestCar '{car_camry_main.id}'"
7181
in result.errors[0].message
7282
)
83+
assert result.data
7384
assert result.data["TestPersonDelete"] is None
7485

7586
assert await NodeManager.get_one(db=db, id=person_jane_main.id) is not None
@@ -109,6 +120,7 @@ async def test_delete_allowed_when_peer_rel_optional_on_generic(
109120
)
110121

111122
assert result.errors is None
123+
assert result.data
112124
assert result.data["TestPersonDelete"]["ok"] is True
113125

114126
updated_dog1 = await NodeManager.get_one(db=db, id=dog1.id)
@@ -151,3 +163,68 @@ async def test_delete_prevented_when_peer_rel_required_on_generic(
151163
assert result.errors
152164
assert len(result.errors) == 1
153165
assert expected_error_message in result.errors[0].message
166+
167+
168+
async def test_delete_events_with_cascade(
169+
db,
170+
default_branch: Branch,
171+
dependent_generics_schema: SchemaBranch,
172+
enable_broker_config: None,
173+
session_first_account: AccountSession,
174+
) -> None:
175+
# set TestPerson.animals to be cascade delete
176+
schema_branch = registry.schema.get_schema_branch(name=default_branch.name)
177+
for schema_kind in ("TestPerson", "TestHuman", "TestCylon"):
178+
schema = schema_branch.get(name=schema_kind, duplicate=False)
179+
schema.get_relationship("animals").on_delete = RelationshipDeleteBehavior.CASCADE
180+
181+
human = await Node.init(db=db, schema="TestHuman", branch=default_branch)
182+
await human.new(db=db, name="Jane", height=180)
183+
await human.save(db=db)
184+
dog = await Node.init(db=db, schema="TestDog", branch=default_branch)
185+
await dog.new(db=db, name="Roofus", breed="whocares", weight=50, owner=human)
186+
await dog.save(db=db)
187+
188+
memory_event = MemoryInfrahubEvent()
189+
service = await InfrahubServices.new(event=memory_event)
190+
gql_params = await prepare_graphql_params(
191+
db=db, include_subscription=False, branch=default_branch, service=service, account_session=session_first_account
192+
)
193+
query = """
194+
mutation DeletePerson($human_id: String!){
195+
TestHumanDelete(data: {id: $human_id}) {
196+
ok
197+
}
198+
}
199+
"""
200+
result = await graphql(
201+
schema=gql_params.schema,
202+
source=query,
203+
context_value=gql_params.context,
204+
root_value=None,
205+
variable_values={"human_id": human.id},
206+
)
207+
208+
assert not result.errors
209+
210+
node_map = await NodeManager.get_many(db=db, ids=[human.id, dog.id])
211+
assert node_map == {}
212+
213+
assert gql_params.context.background
214+
await gql_params.context.background()
215+
assert len(memory_event.events) == 2
216+
primary = memory_event.events[0]
217+
secondary = memory_event.events[1]
218+
assert isinstance(primary, NodeMutatedEvent)
219+
assert isinstance(secondary, NodeMutatedEvent)
220+
assert primary.kind == "TestHuman"
221+
assert primary.node_id == human.id
222+
assert primary.action == MutationAction.DELETED
223+
assert primary.meta.has_children
224+
225+
assert secondary.kind == "TestDog"
226+
assert secondary.node_id == dog.id
227+
assert secondary.action == MutationAction.DELETED
228+
assert not secondary.meta.has_children
229+
assert secondary.meta.parent == primary.meta.id
230+
assert secondary.meta.ancestors == [ParentEvent(id=primary.get_id(), name=primary.get_name())]

0 commit comments

Comments
 (0)