Skip to content

Commit

Permalink
Merge pull request #5116 from opsmill/dga-20241202-fix-5106
Browse files Browse the repository at this point in the history
Add validator when adding a new attribute / relationship to the schema
  • Loading branch information
dgarros authored Dec 3, 2024
2 parents 0c7f1d7 + 506de50 commit 92066dd
Show file tree
Hide file tree
Showing 25 changed files with 616 additions and 15 deletions.
20 changes: 20 additions & 0 deletions backend/infrahub/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ def _process_attrs_rels(
migration_name="node.attribute.add",
)
)
elif path_type == SchemaPathType.RELATIONSHIP:
self.constraints.append(
SchemaUpdateConstraintInfo(
path=SchemaPath( # type: ignore[call-arg]
schema_kind=schema.kind, path_type=path_type, field_name=field_name
),
constraint_name="node.relationship.add",
)
)

for field_name in node_field_diff.removed.keys():
self.migrations.append(
Expand Down Expand Up @@ -272,6 +281,7 @@ def _process_field(
def validate_all(self, migration_map: dict[str, Any], validator_map: dict[str, Any]) -> None:
self.validate_migrations(migration_map=migration_map)
self.validate_constraints(validator_map=validator_map)
self.add_validator_for_migration(validator_map=validator_map)

def validate_migrations(self, migration_map: dict[str, Any]) -> None:
for migration in self.migrations:
Expand All @@ -295,6 +305,16 @@ def validate_constraints(self, validator_map: dict[str, Any]) -> None:
)
)

def add_validator_for_migration(self, validator_map: dict[str, Any]) -> None:
for migration in self.migrations:
if validator_map.get(migration.migration_name, None):
self.constraints.append(
SchemaUpdateConstraintInfo(
path=migration.path,
constraint_name=migration.migration_name,
)
)


class HashableModelDiff(BaseModel):
model_config = ConfigDict(extra="forbid")
Expand Down
4 changes: 4 additions & 0 deletions backend/infrahub/core/validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from .attribute.regex import AttributeRegexChecker
from .attribute.unique import AttributeUniquenessChecker
from .interface import ConstraintCheckerInterface
from .node.attribute import NodeAttributeAddChecker
from .node.generate_profile import NodeGenerateProfileChecker
from .node.hierarchy import NodeHierarchyChecker
from .node.inherit_from import NodeInheritFromChecker
from .node.relationship import NodeRelationshipAddChecker
from .relationship.count import RelationshipCountChecker
from .relationship.optional import RelationshipOptionalChecker
from .relationship.peer import RelationshipPeerChecker
Expand All @@ -35,4 +37,6 @@
"node.parent.update": NodeHierarchyChecker,
"node.children.update": NodeHierarchyChecker,
"node.generate_profile.update": NodeGenerateProfileChecker,
"node.attribute.add": NodeAttributeAddChecker,
"node.relationship.add": NodeRelationshipAddChecker,
}
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/attribute/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeChoicesChecker(ConstraintCheckerInterface):
query_classes = [AttributeChoicesUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/attribute/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeEnumChecker(ConstraintCheckerInterface):
query_classes = [AttributeEnumUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/attribute/kind.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeKindChecker(ConstraintCheckerInterface):
query_classes = [AttributeKindUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/attribute/length.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeLengthChecker(ConstraintCheckerInterface):
query_classes = [AttributeLengthUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

Expand Down
4 changes: 2 additions & 2 deletions backend/infrahub/core/validators/attribute/optional.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeOptionalChecker(ConstraintCheckerInterface):
query_classes = [AttributeOptionalUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

Expand All @@ -75,7 +75,7 @@ def name(self) -> str:
return "attribute.optional.update"

def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
return request.constraint_name == "attribute.optional.update"
return request.constraint_name == self.name

async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
grouped_data_paths_list: list[GroupedDataPaths] = []
Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/attribute/regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeRegexChecker(ConstraintCheckerInterface):
query_classes = [AttributeRegexUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/attribute/unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def get_paths(self) -> GroupedDataPaths:
class AttributeUniquenessChecker(ConstraintCheckerInterface):
query_classes = [AttributeUniqueUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
45 changes: 45 additions & 0 deletions backend/infrahub/core/validators/node/attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Optional

from ..interface import ConstraintCheckerInterface
from ..query import NodeNotPresentValidatorQuery

if TYPE_CHECKING:
from infrahub.core.branch import Branch
from infrahub.core.path import GroupedDataPaths
from infrahub.database import InfrahubDatabase

from ..model import SchemaConstraintValidatorRequest


class NodeAttributeAddChecker(ConstraintCheckerInterface):
query_classes = [NodeNotPresentValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

@property
def name(self) -> str:
return "node.attribute.add"

def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
return request.constraint_name == self.name

async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
grouped_data_paths_list: list[GroupedDataPaths] = []
if not request.schema_path.field_name:
raise ValueError("field_name is not defined")
attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
if attribute_schema.optional is True or attribute_schema.default_value is not None:
return grouped_data_paths_list

for query_class in self.query_classes:
# TODO add exception handling
query = await query_class.init(
db=self.db, branch=self.branch, node_schema=request.node_schema, schema_path=request.schema_path
)
await query.execute(db=self.db)
grouped_data_paths_list.append(await query.get_paths())
return grouped_data_paths_list
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/node/generate_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def get_paths(self) -> GroupedDataPaths:
class NodeGenerateProfileChecker(ConstraintCheckerInterface):
query_classes = [NodeGenerateProfileValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/node/hierarchy.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async def get_paths(self) -> GroupedDataPaths:
class NodeHierarchyChecker(ConstraintCheckerInterface):
query_classes = [NodeHierarchyUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/node/inherit_from.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


class NodeInheritFromChecker(ConstraintCheckerInterface):
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
45 changes: 45 additions & 0 deletions backend/infrahub/core/validators/node/relationship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Optional

from ..interface import ConstraintCheckerInterface
from ..query import NodeNotPresentValidatorQuery

if TYPE_CHECKING:
from infrahub.core.branch import Branch
from infrahub.core.path import GroupedDataPaths
from infrahub.database import InfrahubDatabase

from ..model import SchemaConstraintValidatorRequest


class NodeRelationshipAddChecker(ConstraintCheckerInterface):
query_classes = [NodeNotPresentValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None):
self.db = db
self.branch = branch

@property
def name(self) -> str:
return "node.relationship.add"

def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
return request.constraint_name == self.name

async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
grouped_data_paths_list: list[GroupedDataPaths] = []
if not request.schema_path.field_name:
raise ValueError("field_name is not defined")
rel_schema = request.node_schema.get_relationship(name=request.schema_path.field_name)
if rel_schema.optional is True:
return grouped_data_paths_list

for query_class in self.query_classes:
# TODO add exception handling
query = await query_class.init(
db=self.db, branch=self.branch, node_schema=request.node_schema, schema_path=request.schema_path
)
await query.execute(db=self.db)
grouped_data_paths_list.append(await query.get_paths())
return grouped_data_paths_list
54 changes: 54 additions & 0 deletions backend/infrahub/core/validators/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any

from infrahub.core.constants import PathType
from infrahub.core.path import DataPath, GroupedDataPaths

from .shared import SchemaValidatorQuery

if TYPE_CHECKING:
from infrahub.database import InfrahubDatabase


class NodeNotPresentValidatorQuery(SchemaValidatorQuery):
name: str = "node_not_present_validator"

async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
self.params.update(branch_params)

query = """
MATCH (n:%(node_kind)s)
CALL {
WITH n
MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)
WHERE all(
r in relationships(path)
WHERE %(branch_filter)s
)
RETURN path as full_path, n as node, rr as root_relationship
ORDER BY rr.branch_level DESC, rr.from DESC
LIMIT 1
}
WITH full_path, node, root_relationship
WITH full_path, node, root_relationship
WHERE all(r in relationships(full_path) WHERE r.status = "active")
""" % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}

self.add_to_query(query)
self.return_labels = ["node.uuid", "root_relationship"]

async def get_paths(self) -> GroupedDataPaths:
grouped_data_paths = GroupedDataPaths()
for result in self.results:
grouped_data_paths.add_data_path(
DataPath(
branch=str(result.get("root_relationship").get("branch")),
path_type=PathType.NODE,
node_id=str(result.get("node.uuid")),
kind=self.node_schema.kind,
),
)

return grouped_data_paths
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/relationship/count.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ async def get_paths(self) -> GroupedDataPaths:
class RelationshipCountChecker(ConstraintCheckerInterface):
query_classes = [RelationshipCountUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/relationship/optional.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async def get_paths(self) -> GroupedDataPaths:
class RelationshipOptionalChecker(ConstraintCheckerInterface):
query_classes = [RelationshipOptionalUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/validators/relationship/peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async def get_paths(self) -> GroupedDataPaths:
class RelationshipPeerChecker(ConstraintCheckerInterface):
query_classes = [RelationshipPeerUpdateValidatorQuery]

def __init__(self, db: InfrahubDatabase, branch: Optional[Branch]) -> None:
def __init__(self, db: InfrahubDatabase, branch: Optional[Branch] = None) -> None:
self.db = db
self.branch = branch

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from .attribute_uniqueness import SchemaAttributeUniqueConstraintDependency
from .generate_profile import SchemaGenerateProfileConstraintDependency
from .inherit_from import SchemaInheritFromConstraintDependency
from .node_attribute import SchemaNodeAttributeAddConstraintDependency
from .node_relationship import SchemaNodeRelationshipAddConstraintDependency
from .relationship_count import SchemaRelationshipCountConstraintDependency
from .relationship_optional import SchemaRelationshipOptionalConstraintDependency
from .uniqueness import SchemaUniquenessConstraintDependency
Expand All @@ -30,6 +32,8 @@ def build(cls, context: DependencyBuilderContext) -> AggregatedConstraintChecker
SchemaAttributeChoicesConstraintDependency.build(context=context),
SchemaAttributeEnumConstraintDependency.build(context=context),
SchemaAttributLengthConstraintDependency.build(context=context),
SchemaNodeAttributeAddConstraintDependency.build(context=context),
SchemaNodeRelationshipAddConstraintDependency.build(context=context),
],
db=context.db,
branch=context.branch,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from infrahub.core.validators.node.attribute import NodeAttributeAddChecker
from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext


class SchemaNodeAttributeAddConstraintDependency(DependencyBuilder[NodeAttributeAddChecker]):
@classmethod
def build(cls, context: DependencyBuilderContext) -> NodeAttributeAddChecker:
return NodeAttributeAddChecker(db=context.db, branch=context.branch)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from infrahub.core.validators.node.relationship import NodeRelationshipAddChecker
from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext


class SchemaNodeRelationshipAddConstraintDependency(DependencyBuilder[NodeRelationshipAddChecker]):
@classmethod
def build(cls, context: DependencyBuilderContext) -> NodeRelationshipAddChecker:
return NodeRelationshipAddChecker(db=context.db, branch=context.branch)
Loading

0 comments on commit 92066dd

Please sign in to comment.