Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IFC-605 fix delete constraint to handle relationships on generics #4351

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions backend/infrahub/core/node/delete_validator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict
from enum import Enum
from typing import Iterable, Optional, Union

Expand Down Expand Up @@ -25,7 +26,7 @@ class NodeDeleteIndex:
def __init__(self, all_schemas_map: dict[str, MainSchemaTypes]) -> None:
self._all_schemas_map = all_schemas_map
# {node_schema: {DeleteRelationshipType: {relationship_identifier: peer_node_schema}}}
self._dependency_graph: dict[str, dict[DeleteRelationshipType, dict[str, str]]] = {}
self._dependency_graph: dict[str, dict[DeleteRelationshipType, dict[str, set[str]]]] = {}

def index(self, start_schemas: Iterable[MainSchemaTypes]) -> None:
self._index_cascading_deletes(start_schemas=start_schemas)
Expand All @@ -37,8 +38,8 @@ def _add_to_dependency_graph(
if kind not in self._dependency_graph:
self._dependency_graph[kind] = {}
if relationship_type not in self._dependency_graph[kind]:
self._dependency_graph[kind][relationship_type] = {}
self._dependency_graph[kind][relationship_type][relationship_identifier] = peer_kind
self._dependency_graph[kind][relationship_type] = defaultdict(set)
self._dependency_graph[kind][relationship_type][relationship_identifier].add(peer_kind)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a mandatory relationship is defined on a generic, then there can be multiple peer kinds that use the same relationship identifier, so this data structure needs to be a set instead of a single item that can be overwritten


def _index_cascading_deletes(self, start_schemas: Iterable[MainSchemaTypes]) -> None:
kinds_to_check: set[str] = {schema.kind for schema in start_schemas}
Expand Down Expand Up @@ -77,11 +78,14 @@ def get_relationship_identifiers(self) -> list[FullRelationshipIdentifier]:
full_relationship_identifiers = []
for node_kind, relationship_type_details in self._dependency_graph.items():
for relationship_map in relationship_type_details.values():
for relationship_identifier, peer_kind in relationship_map.items():
full_relationship_identifiers.append(
FullRelationshipIdentifier(
source_kind=node_kind, identifier=relationship_identifier, destination_kind=peer_kind
)
for relationship_identifier, peer_kinds in relationship_map.items():
full_relationship_identifiers.extend(
[
FullRelationshipIdentifier(
source_kind=node_kind, identifier=relationship_identifier, destination_kind=peer_kind
)
for peer_kind in peer_kinds
]
)
return full_relationship_identifiers

Expand Down
78 changes: 78 additions & 0 deletions backend/tests/unit/graphql/test_mutation_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,81 @@ async def test_delete_prevented(
assert result.data["TestPersonDelete"] is None

assert await NodeManager.get_one(db=db, id=person_jane_main.id) is not None


async def test_delete_allowed_when_peer_rel_optional_on_generic(
db: InfrahubDatabase, default_branch, animal_person_schema
):
person_schema = animal_person_schema.get(name="TestPerson")
dog_schema = animal_person_schema.get(name="TestDog")

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

dog1 = await Node.init(db=db, schema=dog_schema, branch=default_branch)
await dog1.new(db=db, name="Rocky", breed="Labrador", owner=person2, best_friend=person1)
await dog1.save(db=db)

query = """
mutation DeletePerson($person_id: String!){
TestPersonDelete(data: {id: $person_id}) {
ok
}
}
"""
gql_params = prepare_graphql_params(db=db, include_subscription=False, branch=default_branch)
result = await graphql(
schema=gql_params.schema,
source=query,
context_value=gql_params.context,
root_value=None,
variable_values={"person_id": person1.id},
)

assert result.errors is None
assert result.data["TestPersonDelete"]["ok"] is True

updated_dog1 = await NodeManager.get_one(db=db, id=dog1.id)
updated_best_friend = await updated_dog1.best_friend.get_peer(db=db)
assert updated_best_friend is None


async def test_delete_prevented_when_peer_rel_required_on_generic(
db: InfrahubDatabase, default_branch, animal_person_schema
):
person_schema = animal_person_schema.get(name="TestPerson")
dog_schema = animal_person_schema.get(name="TestDog")

person1 = await Node.init(db=db, schema=person_schema, branch=default_branch)
await person1.new(db=db, name="Jack")
await person1.save(db=db)

dog1 = await Node.init(db=db, schema=dog_schema, branch=default_branch)
await dog1.new(db=db, name="Rocky", breed="Labrador", owner=person1)
await dog1.save(db=db)

query = """
mutation DeletePerson($person_id: String!){
TestPersonDelete(data: {id: $person_id}) {
ok
}
}
"""
gql_params = prepare_graphql_params(db=db, include_subscription=False, branch=default_branch)
result = await graphql(
schema=gql_params.schema,
source=query,
context_value=gql_params.context,
root_value=None,
variable_values={"person_id": person1.id},
)

expected_error_message = f"Cannot delete TestPerson '{person1.id}'."
expected_error_message += f" It is linked to mandatory relationship owner on node TestDog '{dog1.id}'"
assert result.errors
assert len(result.errors) == 1
assert expected_error_message in result.errors[0].message
1 change: 1 addition & 0 deletions changelog/4207.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deleting a node that is linked to a mandatory relationship on a generic schema will now fail with an error message
Loading