Skip to content

Commit 7504791

Browse files
committed
Add node changelog events for cascade delete mutations
1 parent 593c320 commit 7504791

File tree

5 files changed

+105
-9
lines changed

5 files changed

+105
-9
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ async def mutate_delete(
402402
info: GraphQLResolveInfo,
403403
data: InputObjectType,
404404
branch: Branch,
405-
) -> tuple[Node, Self]:
405+
) -> tuple[Node, Self, list[Node]]:
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 reconciled_prefix, cls(ok=ok), []

backend/infrahub/graphql/mutations/main.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: InputObjectTyp
6464
obj = None
6565
mutation = None
6666
action = MutationAction.UNDEFINED
67+
deleted: list[Node] = []
6768

6869
if "Create" in cls.__name__:
6970
obj, mutation = await cls.mutate_create(info=info, branch=graphql_context.branch, data=data, **kwargs)
@@ -86,7 +87,9 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: InputObjectTyp
8687
else:
8788
action = MutationAction.UPDATED
8889
elif "Delete" in cls.__name__:
89-
obj, mutation = await cls.mutate_delete(info=info, branch=graphql_context.branch, data=data, **kwargs)
90+
obj, mutation, deleted = await cls.mutate_delete(
91+
info=info, branch=graphql_context.branch, data=data, **kwargs
92+
)
9093
action = MutationAction.DELETED
9194
else:
9295
raise ValueError(
@@ -124,18 +127,34 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: InputObjectTyp
124127

125128
events = [main_event]
126129

127-
for node_changelog in node_changelogs:
130+
deleted_changelogs = [node.node_changelog for node in deleted if node.id != obj.id]
131+
deleted_ids = [node.node_id for node in deleted_changelogs]
132+
133+
for node_changelog in deleted_changelogs:
128134
meta = EventMeta.from_parent(parent=main_event)
129135
event = NodeMutatedEvent(
130136
kind=node_changelog.node_kind,
131137
node_id=node_changelog.node_id,
132138
data=node_changelog,
133-
action=MutationAction.UPDATED,
139+
action=MutationAction.DELETED,
134140
fields=node_changelog.updated_fields,
135141
meta=meta,
136142
)
137143
events.append(event)
138144

145+
for node_changelog in node_changelogs:
146+
if node_changelog.node_id not in deleted_ids:
147+
meta = EventMeta.from_parent(parent=main_event)
148+
event = NodeMutatedEvent(
149+
kind=node_changelog.node_kind,
150+
node_id=node_changelog.node_id,
151+
data=node_changelog,
152+
action=MutationAction.UPDATED,
153+
fields=node_changelog.updated_fields,
154+
meta=meta,
155+
)
156+
events.append(event)
157+
139158
for event in events:
140159
graphql_context.background.add_task(graphql_context.active_service.event.send, event)
141160

@@ -494,7 +513,7 @@ async def mutate_delete(
494513
info: GraphQLResolveInfo,
495514
data: InputObjectType,
496515
branch: Branch,
497-
) -> tuple[Node, Self]:
516+
) -> tuple[Node, Self, list[Node]]:
498517
graphql_context: GraphqlContext = info.context
499518

500519
obj = await NodeManager.find_object(
@@ -512,7 +531,7 @@ async def mutate_delete(
512531

513532
ok = True
514533

515-
return obj, cls(ok=ok)
534+
return obj, cls(ok=ok), deleted
516535

517536

518537
class InfrahubMutation(InfrahubMutationMixin, Mutation):

backend/infrahub/graphql/mutations/menu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async def mutate_delete(
8989
info: GraphQLResolveInfo,
9090
data: InputObjectType,
9191
branch: Branch,
92-
) -> tuple[Node, Self]:
92+
) -> tuple[Node, Self, list[Node]]:
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)