Skip to content

Commit f1ce2f6

Browse files
committed
Loading a schema with an incorrect order_by field raise a proper error
1 parent d2f5a69 commit f1ce2f6

File tree

5 files changed

+88
-71
lines changed

5 files changed

+88
-71
lines changed

backend/infrahub/core/constants/schema.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,29 @@ class UpdateValidationErrorType(Enum):
2727

2828

2929
class SchemaElementPathType(Flag):
30-
ATTR = auto()
31-
REL_ONE_NO_ATTR = auto()
32-
REL_ONE_ATTR = auto()
30+
ATTR_WITH_PROP = auto()
31+
ATTR_NO_PROP = auto()
32+
ATTR = ATTR_WITH_PROP | ATTR_NO_PROP
33+
3334
REL_MANY_NO_ATTR = auto()
3435
REL_MANY_ATTR = auto()
35-
ALL_RELS = REL_ONE_NO_ATTR | REL_MANY_NO_ATTR | REL_ONE_ATTR | REL_MANY_ATTR
36-
RELS_ATTR = REL_ONE_ATTR | REL_MANY_ATTR
37-
RELS_NO_ATTR = REL_ONE_NO_ATTR | REL_MANY_NO_ATTR
38-
REL_ONE = REL_ONE_NO_ATTR | REL_ONE_ATTR
3936
REL_MANY = REL_MANY_NO_ATTR | REL_MANY_ATTR
37+
38+
REL_ONE_MANDATORY_NO_ATTR = auto()
39+
REL_ONE_MANDATORY_ATTR_WITH_PROP = auto()
40+
REL_ONE_MANDATORY_ATTR_NO_PROP = auto()
41+
REL_ONE_MANDATORY_ATTR = REL_ONE_MANDATORY_ATTR_WITH_PROP | REL_ONE_MANDATORY_ATTR_NO_PROP
42+
REL_ONE_MANDATORY = REL_ONE_MANDATORY_ATTR | REL_ONE_MANDATORY_NO_ATTR
43+
REL_ONE_OPTIONAL_ATTR_WITH_PROP = auto()
44+
REL_ONE_OPTIONAL_ATTR_NO_PROP = auto()
45+
REL_ONE_OPTIONAL_ATTR = REL_ONE_OPTIONAL_ATTR_WITH_PROP | REL_ONE_OPTIONAL_ATTR_NO_PROP
46+
REL_ONE_OPTIONAL_NO_ATTR = auto()
47+
REL_ONE_OPTIONAL = REL_ONE_OPTIONAL_ATTR | REL_ONE_OPTIONAL_NO_ATTR
48+
REL_ONE = REL_ONE_MANDATORY | REL_ONE_OPTIONAL
49+
REL_ONE_ATTR_WITH_PROP = REL_ONE_MANDATORY_ATTR_WITH_PROP | REL_ONE_OPTIONAL_ATTR_WITH_PROP
50+
REL_ONE_ATTR = REL_ONE_MANDATORY_ATTR | REL_ONE_OPTIONAL_ATTR
51+
REL_ONE_NO_ATTR = REL_ONE_MANDATORY_NO_ATTR | REL_ONE_OPTIONAL_NO_ATTR
52+
53+
REL = REL_ONE | REL_MANY
54+
REL_ATTR = REL_ONE_MANDATORY_ATTR | REL_ONE_OPTIONAL_ATTR | REL_MANY_ATTR
55+
RELS_NO_ATTR = REL_ONE_MANDATORY_NO_ATTR | REL_ONE_OPTIONAL_NO_ATTR | REL_MANY_NO_ATTR

backend/infrahub/core/schema_manager.py

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -595,31 +595,55 @@ def validate_schema_path(
595595
if not (SchemaElementPathType.ATTR & allowed_path_types) and schema_attribute_path.is_type_attribute:
596596
raise ValueError(f"{error_header}: this property only supports relationships not attributes")
597597

598-
if not (SchemaElementPathType.ALL_RELS & allowed_path_types) and schema_attribute_path.is_type_relationship:
598+
if not (SchemaElementPathType.REL & allowed_path_types) and schema_attribute_path.is_type_relationship:
599599
raise ValueError(f"{error_header}: this property only supports attributes, not relationships")
600600

601-
if schema_attribute_path.is_type_relationship and schema_attribute_path.relationship_schema:
602-
if (
603-
schema_attribute_path.relationship_schema.cardinality == RelationshipCardinality.ONE
604-
and not SchemaElementPathType.REL_ONE & allowed_path_types
605-
):
601+
if not (SchemaElementPathType.ATTR_NO_PROP & allowed_path_types) and schema_attribute_path.is_type_attribute:
602+
required_properties = tuple(
603+
schema_attribute_path.attribute_schema.get_class().get_allowed_property_in_path()
604+
)
605+
if schema_attribute_path.attribute_property_name not in required_properties:
606606
raise ValueError(
607-
f"{error_header}: cannot use {schema_attribute_path.relationship_schema.name} relationship,"
608-
" relationship must be of cardinality many"
607+
f"{error_header}: invalid attribute, it must end with one of the following properties:"
608+
f" {', '.join(required_properties)}. (`{path}`)"
609609
)
610+
611+
if schema_attribute_path.is_type_relationship:
612+
if schema_attribute_path.relationship_schema.cardinality == RelationshipCardinality.ONE:
613+
if not SchemaElementPathType.REL_ONE & allowed_path_types:
614+
raise ValueError(
615+
f"{error_header}: cannot use {schema_attribute_path.relationship_schema.name} relationship,"
616+
f" relationship must be of cardinality many. (`{path}`)"
617+
)
618+
if (
619+
not SchemaElementPathType.REL_ONE_OPTIONAL & allowed_path_types
620+
and schema_attribute_path.relationship_schema.optional
621+
and not (
622+
schema_attribute_path.relationship_schema.name == "ip_namespace"
623+
and isinstance(node_schema, NodeSchema)
624+
and (node_schema.is_ip_address() or node_schema.is_ip_prefix)
625+
)
626+
):
627+
raise ValueError(
628+
f"{error_header}: cannot use {schema_attribute_path.relationship_schema.name} relationship,"
629+
f" relationship must be mandatory. (`{path}`)"
630+
)
631+
610632
if (
611633
schema_attribute_path.relationship_schema.cardinality == RelationshipCardinality.MANY
612634
and not SchemaElementPathType.REL_MANY & allowed_path_types
613635
):
614636
raise ValueError(
615637
f"{error_header}: cannot use {schema_attribute_path.relationship_schema.name} relationship,"
616-
" relationship must be of cardinality one"
638+
f" relationship must be of cardinality one (`{path}`)"
617639
)
618640

619-
if schema_attribute_path.has_property and not SchemaElementPathType.RELS_ATTR & allowed_path_types:
620-
raise ValueError(f"{error_header}: cannot use attributes of related node, only the relationship")
641+
if schema_attribute_path.has_property and not SchemaElementPathType.REL_ATTR & allowed_path_types:
642+
raise ValueError(
643+
f"{error_header}: cannot use attributes of related node, only the relationship. (`{path}`)"
644+
)
621645
if not schema_attribute_path.has_property and not SchemaElementPathType.RELS_NO_ATTR & allowed_path_types:
622-
raise ValueError(f"{error_header}: Must use attributes of related node")
646+
raise ValueError(f"{error_header}: Must use attributes of related node. (`{path}`)")
623647

624648
return schema_attribute_path
625649

@@ -669,34 +693,15 @@ def validate_uniqueness_constraints(self) -> None:
669693

670694
for constraint_paths in node_schema.uniqueness_constraints:
671695
for constraint_path in constraint_paths:
672-
schema_path = self.validate_schema_path(
696+
element_name = "uniqueness_constraints"
697+
self.validate_schema_path(
673698
node_schema=node_schema,
674699
path=constraint_path,
675-
allowed_path_types=SchemaElementPathType.ATTR | SchemaElementPathType.REL_ONE_NO_ATTR,
676-
element_name="uniqueness_constraints",
700+
allowed_path_types=SchemaElementPathType.ATTR_WITH_PROP
701+
| SchemaElementPathType.REL_ONE_MANDATORY_NO_ATTR,
702+
element_name=element_name,
677703
)
678704

679-
if schema_path.is_type_attribute:
680-
required_properties = tuple(
681-
schema_path.attribute_schema.get_class().get_allowed_property_in_path()
682-
)
683-
if schema_path.attribute_property_name not in required_properties:
684-
raise ValueError(
685-
f"{node_schema.kind} uniqueness contraint is invalid at attribute '{schema_path.attribute_schema.name}', it must "
686-
f"end with one of the following properties: {', '.join(required_properties)}"
687-
)
688-
689-
if schema_path.is_type_relationship and schema_path.relationship_schema:
690-
if schema_path.relationship_schema.optional and not (
691-
schema_path.relationship_schema.name == "ip_namespace"
692-
and isinstance(node_schema, NodeSchema)
693-
and (node_schema.is_ip_address() or node_schema.is_ip_prefix)
694-
):
695-
raise ValueError(
696-
f"Only mandatory relation of cardinality one can be used in uniqueness_constraints,"
697-
f" {schema_path.relationship_schema.name} is not mandatory. ({constraint_path})"
698-
)
699-
700705
def validate_display_labels(self) -> None:
701706
for name in self.all_names:
702707
node_schema = self.get(name=name, duplicate=False)
@@ -727,13 +732,14 @@ def validate_order_by(self) -> None:
727732
if not node_schema.order_by:
728733
continue
729734

730-
allowed_types = SchemaElementPathType.ATTR | SchemaElementPathType.REL_ONE
735+
allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_ATTR_WITH_PROP
731736
for order_by_path in node_schema.order_by:
737+
element_name = "order_by"
732738
self.validate_schema_path(
733739
node_schema=node_schema,
734740
path=order_by_path,
735741
allowed_path_types=allowed_types,
736-
element_name="order_by",
742+
element_name=element_name,
737743
)
738744

739745
def validate_default_filters(self) -> None:
@@ -778,36 +784,18 @@ def validate_human_friendly_id(self) -> None:
778784
if not node_schema.human_friendly_id:
779785
continue
780786

781-
allowed_types = SchemaElementPathType.ATTR | SchemaElementPathType.REL_ONE_ATTR
782-
for item in node_schema.human_friendly_id:
787+
allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_MANDATORY_ATTR
788+
for hfid_path in node_schema.human_friendly_id:
783789
schema_path = self.validate_schema_path(
784790
node_schema=node_schema,
785-
path=item,
791+
path=hfid_path,
786792
allowed_path_types=allowed_types,
787793
element_name="human_friendly_id",
788794
)
789795

790796
if schema_path.is_type_attribute:
791-
required_properties = tuple(schema_path.attribute_schema.get_class().get_allowed_property_in_path())
792-
if schema_path.attribute_property_name not in required_properties:
793-
raise ValueError(
794-
f"{node_schema.kind} HFID is invalid at attribute '{schema_path.attribute_schema.name}', it must end with one of the "
795-
f"following properties: {', '.join(required_properties)}"
796-
)
797797
hf_attr_names.add(schema_path.attribute_schema.name)
798798

799-
if schema_path.is_type_relationship and schema_path.relationship_schema:
800-
if schema_path.relationship_schema.optional and not (
801-
schema_path.relationship_schema.name == "ip_namespace"
802-
and isinstance(node_schema, NodeSchema)
803-
and (node_schema.is_ip_address() or node_schema.is_ip_prefix)
804-
):
805-
raise ValueError(
806-
f"Only mandatory relationship of cardinality one can be used in human_friendly_id, "
807-
f"{schema_path.relationship_schema.name} is not mandatory on {schema_path.relationship_schema.kind} for "
808-
f"{node_schema.kind}. ({item})"
809-
)
810-
811799
def validate_required_relationships(self) -> None:
812800
reverse_dependency_map: dict[str, set[str]] = {}
813801
for name in self.node_names + self.generic_names:

backend/tests/unit/core/schema_manager/test_manager_schema.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import re
23
import uuid
34

45
import pytest
@@ -1155,11 +1156,17 @@ async def test_validate_exception_ipam_ip_namespace(
11551156
),
11561157
(
11571158
[["mybool__value", "status__name__value"]],
1158-
"InfraGenericInterface.uniqueness_constraints: cannot use attributes of related node, only the relationship",
1159+
"InfraGenericInterface.uniqueness_constraints: cannot use status relationship, relationship must be mandatory. (`status__name__value`)",
11591160
),
11601161
(
11611162
[["mybool", "status__name__value"]],
1162-
"InfraGenericInterface uniqueness contraint is invalid at attribute 'mybool', it must end with one of the following properties: value",
1163+
"InfraGenericInterface.uniqueness_constraints: invalid attribute, "
1164+
"it must end with one of the following properties: value. (`mybool`)",
1165+
),
1166+
(
1167+
[["status__name"]],
1168+
"InfraGenericInterface.uniqueness_constraints: cannot use status relationship, "
1169+
"relationship must be mandatory. (`status__name`)",
11631170
),
11641171
],
11651172
)
@@ -1170,7 +1177,7 @@ async def test_validate_uniqueness_constraints_error(schema_all_in_one, uniquene
11701177
schema = SchemaBranch(cache={}, name="test")
11711178
schema.load_schema(schema=SchemaRoot(**schema_all_in_one))
11721179

1173-
with pytest.raises(ValueError, match=expected_error):
1180+
with pytest.raises(ValueError, match=re.escape(expected_error)):
11741181
schema.validate_uniqueness_constraints()
11751182

11761183

@@ -1263,6 +1270,11 @@ async def test_validate_order_by_success(schema_all_in_one, order_by):
12631270
"InfraGenericInterface.order_by: cannot use badges relationship, relationship must be of cardinality one",
12641271
),
12651272
(["status__name__nothing"], "InfraGenericInterface.order_by: nothing is not a valid property of name"),
1273+
(
1274+
["my_generic_name"],
1275+
"InfraGenericInterface.order_by: invalid attribute, it must end "
1276+
"with one of the following properties: value. (`my_generic_name`)",
1277+
),
12661278
],
12671279
)
12681280
async def test_validate_order_by_error(schema_all_in_one, order_by, expected_error):
@@ -1272,7 +1284,7 @@ async def test_validate_order_by_error(schema_all_in_one, order_by, expected_err
12721284
schema = SchemaBranch(cache={}, name="test")
12731285
schema.load_schema(schema=SchemaRoot(**schema_all_in_one))
12741286

1275-
with pytest.raises(ValueError, match=expected_error):
1287+
with pytest.raises(ValueError, match=re.escape(expected_error)):
12761288
schema.validate_order_by()
12771289

12781290

backend/tests/unit/core/test_relationship.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def test_relationship_init(
3636
assert rel.node_id == person_jack_main.id
3737

3838
rel_node = await rel.get_node(db=db)
39-
assert type(rel_node) == Node
39+
assert type(rel_node) is Node
4040
assert rel_node.id == person_jack_main.id
4141

4242

changelog/4323.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Loading a schema with an invalid order_by field raise a proper error

0 commit comments

Comments
 (0)