infrahub-server 1.2.3__py3-none-any.whl → 1.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. infrahub/cli/git_agent.py +4 -10
  2. infrahub/config.py +32 -0
  3. infrahub/core/constants/__init__.py +1 -0
  4. infrahub/core/constraint/node/runner.py +6 -5
  5. infrahub/core/graph/__init__.py +1 -1
  6. infrahub/core/migrations/graph/__init__.py +2 -0
  7. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +68 -70
  8. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +26 -0
  9. infrahub/core/migrations/schema/node_attribute_remove.py +16 -2
  10. infrahub/core/models.py +1 -1
  11. infrahub/core/node/constraints/grouped_uniqueness.py +6 -1
  12. infrahub/core/registry.py +18 -0
  13. infrahub/core/schema/basenode_schema.py +21 -1
  14. infrahub/core/schema/definitions/internal.py +2 -1
  15. infrahub/core/schema/generated/base_node_schema.py +1 -1
  16. infrahub/core/schema/manager.py +21 -0
  17. infrahub/core/schema/schema_branch.py +3 -4
  18. infrahub/database/__init__.py +10 -0
  19. infrahub/events/group_action.py +6 -1
  20. infrahub/events/node_action.py +5 -1
  21. infrahub/graphql/mutations/main.py +10 -12
  22. infrahub/message_bus/messages/__init__.py +0 -2
  23. infrahub/message_bus/operations/__init__.py +0 -1
  24. infrahub/message_bus/operations/event/__init__.py +2 -2
  25. infrahub/server.py +6 -11
  26. infrahub/services/adapters/cache/__init__.py +17 -0
  27. infrahub/services/adapters/cache/redis.py +11 -1
  28. infrahub/services/adapters/message_bus/__init__.py +20 -0
  29. infrahub/services/component.py +1 -2
  30. infrahub/tasks/registry.py +3 -7
  31. infrahub/workers/infrahub_async.py +4 -10
  32. infrahub_sdk/schema/__init__.py +10 -1
  33. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/METADATA +1 -1
  34. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/RECORD +39 -40
  35. infrahub_testcontainers/container.py +4 -0
  36. infrahub_testcontainers/helpers.py +1 -1
  37. infrahub/message_bus/messages/event_worker_newprimaryapi.py +0 -9
  38. infrahub/message_bus/operations/event/worker.py +0 -9
  39. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/LICENSE.txt +0 -0
  40. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/WHEEL +0 -0
  41. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/entry_points.txt +0 -0
infrahub/cli/git_agent.py CHANGED
@@ -19,10 +19,8 @@ from infrahub.git import initialize_repositories_directory
19
19
  from infrahub.lock import initialize_lock
20
20
  from infrahub.log import get_logger
21
21
  from infrahub.services import InfrahubServices
22
- from infrahub.services.adapters.cache.nats import NATSCache
23
- from infrahub.services.adapters.cache.redis import RedisCache
24
- from infrahub.services.adapters.message_bus.nats import NATSMessageBus
25
- from infrahub.services.adapters.message_bus.rabbitmq import RabbitMQMessageBus
22
+ from infrahub.services.adapters.cache import InfrahubCache
23
+ from infrahub.services.adapters.message_bus import InfrahubMessageBus
26
24
  from infrahub.services.adapters.workflow.local import WorkflowLocalExecution
27
25
  from infrahub.services.adapters.workflow.worker import WorkflowWorkerExecution
28
26
  from infrahub.trace import configure_trace
@@ -121,13 +119,9 @@ async def start(
121
119
 
122
120
  component_type = ComponentType.GIT_AGENT
123
121
  message_bus = config.OVERRIDE.message_bus or (
124
- await NATSMessageBus.new(component_type=component_type)
125
- if config.SETTINGS.broker.driver == config.BrokerDriver.NATS
126
- else await RabbitMQMessageBus.new(component_type=component_type)
127
- )
128
- cache = config.OVERRIDE.cache or (
129
- await NATSCache.new() if config.SETTINGS.cache.driver == config.CacheDriver.NATS else RedisCache()
122
+ await InfrahubMessageBus.new_from_driver(component_type=component_type, driver=config.SETTINGS.broker.driver)
130
123
  )
124
+ cache = config.OVERRIDE.cache or (await InfrahubCache.new_from_driver(driver=config.SETTINGS.cache.driver))
131
125
 
132
126
  service = await InfrahubServices.new(
133
127
  cache=cache,
infrahub/config.py CHANGED
@@ -115,11 +115,43 @@ class BrokerDriver(str, Enum):
115
115
  RabbitMQ = "rabbitmq"
116
116
  NATS = "nats"
117
117
 
118
+ @property
119
+ def driver_module_path(self) -> str:
120
+ match self:
121
+ case BrokerDriver.NATS:
122
+ return "infrahub.services.adapters.message_bus.nats"
123
+ case BrokerDriver.RabbitMQ:
124
+ return "infrahub.services.adapters.message_bus.rabbitmq"
125
+
126
+ @property
127
+ def driver_class_name(self) -> str:
128
+ match self:
129
+ case BrokerDriver.NATS:
130
+ return "NATSMessageBus"
131
+ case BrokerDriver.RabbitMQ:
132
+ return "RabbitMQMessageBus"
133
+
118
134
 
119
135
  class CacheDriver(str, Enum):
120
136
  Redis = "redis"
121
137
  NATS = "nats"
122
138
 
139
+ @property
140
+ def driver_module_path(self) -> str:
141
+ match self:
142
+ case CacheDriver.NATS:
143
+ return "infrahub.services.adapters.cache.nats"
144
+ case CacheDriver.Redis:
145
+ return "infrahub.services.adapters.cache.redis"
146
+
147
+ @property
148
+ def driver_class_name(self) -> str:
149
+ match self:
150
+ case CacheDriver.NATS:
151
+ return "NATSCache"
152
+ case CacheDriver.Redis:
153
+ return "RedisCache"
154
+
123
155
 
124
156
  class WorkflowDriver(str, Enum):
125
157
  LOCAL = "local"
@@ -348,6 +348,7 @@ RESTRICTED_NAMESPACES: list[str] = [
348
348
  NODE_NAME_REGEX = r"^[A-Z][a-zA-Z0-9]+$"
349
349
  DEFAULT_NAME_MIN_LENGTH = 2
350
350
  NAME_REGEX = r"^[a-z0-9\_]+$"
351
+ NAME_REGEX_OR_EMPTY = r"^[a-z0-9\_]*$"
351
352
  DEFAULT_DESCRIPTION_LENGTH = 128
352
353
 
353
354
  DEFAULT_NAME_MAX_LENGTH = 32
@@ -23,10 +23,15 @@ class NodeConstraintRunner:
23
23
  self.uniqueness_constraint = uniqueness_constraint
24
24
  self.relationship_manager_constraints = relationship_manager_constraints
25
25
 
26
- async def check(self, node: Node, field_filters: list[str] | None = None) -> None:
26
+ async def check(
27
+ self, node: Node, field_filters: list[str] | None = None, skip_uniqueness_check: bool = False
28
+ ) -> None:
27
29
  async with self.db.start_session() as db:
28
30
  await node.resolve_relationships(db=db)
29
31
 
32
+ if not skip_uniqueness_check:
33
+ await self.uniqueness_constraint.check(node, filters=field_filters)
34
+
30
35
  for relationship_name in node.get_schema().relationship_names:
31
36
  if field_filters and relationship_name not in field_filters:
32
37
  continue
@@ -34,7 +39,3 @@ class NodeConstraintRunner:
34
39
  await relationship_manager.fetch_relationship_ids(db=db, force_refresh=True)
35
40
  for relationship_constraint in self.relationship_manager_constraints:
36
41
  await relationship_constraint.check(relm=relationship_manager, node_schema=node.get_schema())
37
-
38
- # If HFID constraint is the only constraint violated, all other constraints need to have ran before,
39
- # as it means there is an existing node that we might want to update in the case of an upsert
40
- await self.uniqueness_constraint.check(node, filters=field_filters)
@@ -1 +1 @@
1
- GRAPH_VERSION = 24
1
+ GRAPH_VERSION = 25
@@ -26,6 +26,7 @@ from .m021_missing_hierarchy_merge import Migration021
26
26
  from .m022_add_generate_template_attr import Migration022
27
27
  from .m023_deduplicate_cardinality_one_relationships import Migration023
28
28
  from .m024_missing_hierarchy_backfill import Migration024
29
+ from .m025_uniqueness_nulls import Migration025
29
30
 
30
31
  if TYPE_CHECKING:
31
32
  from infrahub.core.root import Root
@@ -57,6 +58,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
57
58
  Migration022,
58
59
  Migration023,
59
60
  Migration024,
61
+ Migration025,
60
62
  ]
61
63
 
62
64
 
@@ -21,81 +21,79 @@ if TYPE_CHECKING:
21
21
  log = get_logger()
22
22
 
23
23
 
24
+ async def validate_nulls_in_uniqueness_constraints(db: InfrahubDatabase) -> MigrationResult:
25
+ """
26
+ Validate any schema that include optional attributes in the uniqueness constraints
27
+
28
+ An update to uniqueness constraint validation now handles NULL values as unique instead of ignoring them
29
+ """
30
+
31
+ default_branch = registry.get_branch_from_registry()
32
+ build_component_registry()
33
+ component_registry = get_component_registry()
34
+ uniqueness_checker = await component_registry.get_component(UniquenessChecker, db=db, branch=default_branch)
35
+ non_unique_nodes_by_kind: dict[str, list[NonUniqueNode]] = defaultdict(list)
36
+
37
+ manager = SchemaManager()
38
+ registry.schema = manager
39
+ internal_schema_root = SchemaRoot(**internal_schema)
40
+ manager.register_schema(schema=internal_schema_root)
41
+ schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
42
+ manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
43
+
44
+ for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
45
+ schema = schema_branch.get(name=schema_kind, duplicate=False)
46
+ if not isinstance(schema, NodeSchema | GenericSchema):
47
+ continue
48
+
49
+ uniqueness_constraint_paths = schema.get_unique_constraint_schema_attribute_paths(schema_branch=schema_branch)
50
+ includes_optional_attr: bool = False
51
+
52
+ for uniqueness_constraint_path in uniqueness_constraint_paths:
53
+ for schema_attribute_path in uniqueness_constraint_path.attributes_paths:
54
+ if schema_attribute_path.attribute_schema and schema_attribute_path.attribute_schema.optional is True:
55
+ includes_optional_attr = True
56
+ break
57
+
58
+ if not includes_optional_attr:
59
+ continue
60
+
61
+ non_unique_nodes = await uniqueness_checker.check_one_schema(schema=schema)
62
+ if non_unique_nodes:
63
+ non_unique_nodes_by_kind[schema_kind] = non_unique_nodes
64
+
65
+ if not non_unique_nodes_by_kind:
66
+ return MigrationResult()
67
+
68
+ error_strings = []
69
+ for schema_kind, non_unique_nodes in non_unique_nodes_by_kind.items():
70
+ display_label_map = await get_display_labels_per_kind(
71
+ db=db, kind=schema_kind, branch_name=default_branch.name, ids=[nun.node_id for nun in non_unique_nodes]
72
+ )
73
+ for non_unique_node in non_unique_nodes:
74
+ display_label = display_label_map.get(non_unique_node.node_id)
75
+ error_str = f"{display_label or ''}({non_unique_node.node_schema.kind} / {non_unique_node.node_id})"
76
+ error_str += " violates uniqueness constraints for the following attributes: "
77
+ attr_values = [
78
+ f"{attr.attribute_name}={attr.attribute_value}" for attr in non_unique_node.non_unique_attributes
79
+ ]
80
+ error_str += ", ".join(attr_values)
81
+ error_strings.append(error_str)
82
+ if error_strings:
83
+ error_str = "For the following nodes, you must update the uniqueness_constraints on the schema of the node"
84
+ error_str += " to remove the attribute(s) with NULL values or update the data on the nodes to be unique"
85
+ error_str += " now that NULL values are considered during uniqueness validation"
86
+ return MigrationResult(errors=[error_str] + error_strings)
87
+ return MigrationResult()
88
+
89
+
24
90
  class Migration018(InternalSchemaMigration):
25
91
  name: str = "018_validate_nulls_in_uniqueness_constraints"
26
92
  minimum_version: int = 17
27
93
  migrations: Sequence[SchemaMigration] = []
28
94
 
29
95
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
30
- result = MigrationResult()
31
-
32
- return result
96
+ return MigrationResult()
33
97
 
34
98
  async def execute(self, db: InfrahubDatabase) -> MigrationResult:
35
- """
36
- Validate any schema that include optional attributes in the uniqueness constraints
37
-
38
- An update to uniqueness constraint validation now handles NULL values as unique instead of ignoring them
39
- """
40
- default_branch = registry.get_branch_from_registry()
41
- build_component_registry()
42
- component_registry = get_component_registry()
43
- uniqueness_checker = await component_registry.get_component(UniquenessChecker, db=db, branch=default_branch)
44
- non_unique_nodes_by_kind: dict[str, list[NonUniqueNode]] = defaultdict(list)
45
-
46
- manager = SchemaManager()
47
- registry.schema = manager
48
- internal_schema_root = SchemaRoot(**internal_schema)
49
- manager.register_schema(schema=internal_schema_root)
50
- schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
51
- manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
52
-
53
- for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
54
- schema = schema_branch.get(name=schema_kind, duplicate=False)
55
- if not isinstance(schema, NodeSchema | GenericSchema):
56
- continue
57
-
58
- uniqueness_constraint_paths = schema.get_unique_constraint_schema_attribute_paths(
59
- schema_branch=schema_branch
60
- )
61
- includes_optional_attr: bool = False
62
-
63
- for uniqueness_constraint_path in uniqueness_constraint_paths:
64
- for schema_attribute_path in uniqueness_constraint_path.attributes_paths:
65
- if (
66
- schema_attribute_path.attribute_schema
67
- and schema_attribute_path.attribute_schema.optional is True
68
- ):
69
- includes_optional_attr = True
70
- break
71
-
72
- if not includes_optional_attr:
73
- continue
74
-
75
- non_unique_nodes = await uniqueness_checker.check_one_schema(schema=schema)
76
- if non_unique_nodes:
77
- non_unique_nodes_by_kind[schema_kind] = non_unique_nodes
78
-
79
- if not non_unique_nodes_by_kind:
80
- return MigrationResult()
81
-
82
- error_strings = []
83
- for schema_kind, non_unique_nodes in non_unique_nodes_by_kind.items():
84
- display_label_map = await get_display_labels_per_kind(
85
- db=db, kind=schema_kind, branch_name=default_branch.name, ids=[nun.node_id for nun in non_unique_nodes]
86
- )
87
- for non_unique_node in non_unique_nodes:
88
- display_label = display_label_map.get(non_unique_node.node_id)
89
- error_str = f"{display_label or ''}({non_unique_node.node_schema.kind} / {non_unique_node.node_id})"
90
- error_str += " violates uniqueness constraints for the following attributes: "
91
- attr_values = [
92
- f"{attr.attribute_name}={attr.attribute_value}" for attr in non_unique_node.non_unique_attributes
93
- ]
94
- error_str += ", ".join(attr_values)
95
- error_strings.append(error_str)
96
- if error_strings:
97
- error_str = "For the following nodes, you must update the uniqueness_constraints on the schema of the node"
98
- error_str += " to remove the attribute(s) with NULL values or update the data on the nodes to be unique"
99
- error_str += " now that NULL values are considered during uniqueness validation"
100
- return MigrationResult(errors=[error_str] + error_strings)
101
- return MigrationResult()
99
+ return await validate_nulls_in_uniqueness_constraints(db=db)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from infrahub.core.migrations.shared import MigrationResult
6
+ from infrahub.log import get_logger
7
+
8
+ from ..shared import InternalSchemaMigration, SchemaMigration
9
+ from .m018_uniqueness_nulls import validate_nulls_in_uniqueness_constraints
10
+
11
+ if TYPE_CHECKING:
12
+ from infrahub.database import InfrahubDatabase
13
+
14
+ log = get_logger()
15
+
16
+
17
+ class Migration025(InternalSchemaMigration):
18
+ name: str = "025_validate_nulls_in_uniqueness_constraints"
19
+ minimum_version: int = 24
20
+ migrations: Sequence[SchemaMigration] = []
21
+
22
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
23
+ return MigrationResult()
24
+
25
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
26
+ return await validate_nulls_in_uniqueness_constraints(db=db)
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from infrahub.core.constants import RelationshipStatus
6
6
  from infrahub.core.graph.schema import GraphAttributeRelationships
7
+ from infrahub.core.schema.generic_schema import GenericSchema
7
8
 
8
9
  from ..query import AttributeMigrationQuery
9
10
  from ..shared import AttributeSchemaMigration
@@ -22,8 +23,20 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
22
23
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
23
24
  self.params.update(branch_params)
24
25
 
26
+ attr_name = self.migration.schema_path.field_name
27
+ kinds_to_ignore = []
28
+ if isinstance(self.migration.new_node_schema, GenericSchema) and attr_name is not None:
29
+ for inheriting_schema_kind in self.migration.new_node_schema.used_by:
30
+ node_schema = db.schema.get_node_schema(
31
+ name=inheriting_schema_kind, branch=self.branch, duplicate=False
32
+ )
33
+ attr_schema = node_schema.get_attribute_or_none(name=attr_name)
34
+ if attr_schema and not attr_schema.inherited:
35
+ kinds_to_ignore.append(inheriting_schema_kind)
36
+
25
37
  self.params["node_kind"] = self.migration.new_schema.kind
26
- self.params["attr_name"] = self.migration.schema_path.field_name
38
+ self.params["kinds_to_ignore"] = kinds_to_ignore
39
+ self.params["attr_name"] = attr_name
27
40
  self.params["current_time"] = self.at.to_string()
28
41
  self.params["branch_name"] = self.branch.name
29
42
  self.params["branch_support"] = self.migration.previous_attribute_schema.get_branch().value
@@ -60,7 +73,8 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
60
73
  query = """
61
74
  // Find all the active nodes
62
75
  MATCH (node:%(node_kind)s)
63
- WHERE exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
76
+ WHERE (size($kinds_to_ignore) = 0 OR NOT any(l IN labels(node) WHERE l IN $kinds_to_ignore))
77
+ AND exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
64
78
  CALL {
65
79
  WITH node
66
80
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
infrahub/core/models.py CHANGED
@@ -529,7 +529,7 @@ class HashableModel(BaseModel):
529
529
 
530
530
  return new_list
531
531
 
532
- def update(self, other: Self) -> Self:
532
+ def update(self, other: HashableModel) -> Self:
533
533
  """Update the current object with the new value from the new one if they are defined.
534
534
 
535
535
  Currently this method works for the following type of fields
@@ -220,8 +220,13 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
220
220
 
221
221
  violations = []
222
222
  for schema in schemas_to_check:
223
+ schema_filters = list(filters) if filters is not None else []
224
+ for attr_schema in schema.attributes:
225
+ if attr_schema.optional and attr_schema.unique and attr_schema.name not in schema_filters:
226
+ schema_filters.append(attr_schema.name)
227
+
223
228
  schema_violations = await self._get_single_schema_violations(
224
- node=node, node_schema=schema, at=at, filters=filters
229
+ node=node, node_schema=schema, at=at, filters=schema_filters
225
230
  )
226
231
  violations.extend(schema_violations)
227
232
 
infrahub/core/registry.py CHANGED
@@ -220,5 +220,23 @@ class Registry:
220
220
  and branch.active_schema_hash.main != default_branch.active_schema_hash.main
221
221
  ]
222
222
 
223
+ async def purge_inactive_branches(
224
+ self, db: InfrahubDatabase, active_branches: list[Branch] | None = None
225
+ ) -> list[str]:
226
+ """Return a list of branches that were purged from the registry."""
227
+ active_branches = active_branches or await self.branch_object.get_list(db=db)
228
+ active_branch_names = [branch.name for branch in active_branches]
229
+ purged_branches: set[str] = set()
230
+
231
+ for branch_name in list(registry.branch.keys()):
232
+ if branch_name not in active_branch_names:
233
+ del registry.branch[branch_name]
234
+ purged_branches.add(branch_name)
235
+
236
+ purged_branches.update(self.schema.purge_inactive_branches(active_branches=active_branch_names))
237
+ purged_branches.update(db.purge_inactive_schemas(active_branches=active_branch_names))
238
+
239
+ return sorted(purged_branches)
240
+
223
241
 
224
242
  registry = Registry()
@@ -11,7 +11,7 @@ from infrahub_sdk.utils import compare_lists, intersection
11
11
  from pydantic import field_validator
12
12
 
13
13
  from infrahub.core.constants import RelationshipCardinality, RelationshipKind
14
- from infrahub.core.models import HashableModelDiff
14
+ from infrahub.core.models import HashableModel, HashableModelDiff
15
15
 
16
16
  from .attribute_schema import AttributeSchema
17
17
  from .generated.base_node_schema import GeneratedBaseNodeSchema
@@ -27,6 +27,16 @@ if TYPE_CHECKING:
27
27
  NODE_METADATA_ATTRIBUTES = ["_source", "_owner"]
28
28
  INHERITED = "INHERITED"
29
29
 
30
+ OPTIONAL_TEXT_FIELDS = [
31
+ "default_filter",
32
+ "description",
33
+ "label",
34
+ "menu_placement",
35
+ "documentation",
36
+ "parent",
37
+ "children",
38
+ ]
39
+
30
40
 
31
41
  class BaseNodeSchema(GeneratedBaseNodeSchema):
32
42
  _exclude_from_hash: list[str] = ["attributes", "relationships"]
@@ -480,6 +490,16 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
480
490
  return UniquenessConstraintType.SUBSET_OF_HFID
481
491
  return UniquenessConstraintType.STANDARD
482
492
 
493
+ def update(self, other: HashableModel) -> Self:
494
+ super().update(other=other)
495
+
496
+ # Allow to specify empty string to remove existing fields values
497
+ for field_name in OPTIONAL_TEXT_FIELDS:
498
+ if getattr(other, field_name, None) == "": # noqa: PLC1901
499
+ setattr(self, field_name, None)
500
+
501
+ return self
502
+
483
503
 
484
504
  @dataclass
485
505
  class SchemaUniquenessConstraintPath:
@@ -18,6 +18,7 @@ from infrahub.core.constants import (
18
18
  DEFAULT_NAME_MIN_LENGTH,
19
19
  DEFAULT_REL_IDENTIFIER_LENGTH,
20
20
  NAME_REGEX,
21
+ NAME_REGEX_OR_EMPTY,
21
22
  NAMESPACE_REGEX,
22
23
  NODE_KIND_REGEX,
23
24
  NODE_NAME_REGEX,
@@ -269,7 +270,7 @@ base_node_schema = SchemaNode(
269
270
  SchemaAttribute(
270
271
  name="default_filter",
271
272
  kind="Text",
272
- regex=str(NAME_REGEX),
273
+ regex=str(NAME_REGEX_OR_EMPTY),
273
274
  description="Default filter used to search for a node in addition to its ID. (deprecated: please use human_friendly_id instead)",
274
275
  optional=True,
275
276
  extra={"update": UpdateSupport.ALLOWED},
@@ -50,7 +50,7 @@ class GeneratedBaseNodeSchema(HashableModel):
50
50
  default_filter: str | None = Field(
51
51
  default=None,
52
52
  description="Default filter used to search for a node in addition to its ID. (deprecated: please use human_friendly_id instead)",
53
- pattern=r"^[a-z0-9\_]+$",
53
+ pattern=r"^[a-z0-9\_]*$",
54
54
  json_schema_extra={"update": "allowed"},
55
55
  )
56
56
  human_friendly_id: list[str] | None = Field(
@@ -744,3 +744,24 @@ class SchemaManager(NodeManager):
744
744
  """Convert a schema_node object loaded from the database into GenericSchema object."""
745
745
  node_data = await cls._prepare_node_data(schema_node=schema_node, db=db)
746
746
  return GenericSchema(**node_data)
747
+
748
+ def purge_inactive_branches(self, active_branches: list[str]) -> list[str]:
749
+ """Return non active branches that were purged."""
750
+
751
+ hashes_to_keep: set[str] = set()
752
+ for active_branch in active_branches:
753
+ if branch := self._branches.get(active_branch):
754
+ nodes = branch.get_all(include_internal=True, duplicate=False)
755
+ hashes_to_keep.update([node.get_hash() for node in nodes.values()])
756
+
757
+ removed_branches: list[str] = []
758
+ for branch_name in list(self._branches.keys()):
759
+ if branch_name not in active_branches:
760
+ del self._branches[branch_name]
761
+ removed_branches.append(branch_name)
762
+
763
+ for hash_key in list(self._cache.keys()):
764
+ if hash_key not in hashes_to_keep:
765
+ del self._cache[hash_key]
766
+
767
+ return removed_branches
@@ -1955,10 +1955,9 @@ class SchemaBranch:
1955
1955
  )
1956
1956
  )
1957
1957
 
1958
- if relationship.kind == RelationshipKind.PARENT:
1959
- template_schema.human_friendly_id = [
1960
- f"{relationship.name}__template_name__value"
1961
- ] + template_schema.human_friendly_id
1958
+ parent_hfid = f"{relationship.name}__template_name__value"
1959
+ if relationship.kind == RelationshipKind.PARENT and parent_hfid not in template_schema.human_friendly_id:
1960
+ template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
1962
1961
  template_schema.uniqueness_constraints[0].append(relationship.name)
1963
1962
 
1964
1963
  def generate_object_template_from_node(
@@ -205,6 +205,16 @@ class InfrahubDatabase:
205
205
  def add_schema(self, schema: SchemaBranch, name: str | None = None) -> None:
206
206
  self._schemas[name or schema.name] = schema
207
207
 
208
+ def purge_inactive_schemas(self, active_branches: list[str]) -> list[str]:
209
+ """Return non active schema branches that were purged."""
210
+ removed_branches: list[str] = []
211
+ for branch_name in list(self._schemas.keys()):
212
+ if branch_name not in active_branches:
213
+ del self._schemas[branch_name]
214
+ removed_branches.append(branch_name)
215
+
216
+ return removed_branches
217
+
208
218
  def start_session(self, read_only: bool = False, schemas: list[SchemaBranch] | None = None) -> InfrahubDatabase:
209
219
  """Create a new InfrahubDatabase object in Session mode."""
210
220
  session_mode = InfrahubDatabaseSessionMode.WRITE
@@ -2,7 +2,7 @@ from typing import ClassVar
2
2
 
3
3
  from pydantic import Field
4
4
 
5
- from infrahub.core.constants import MutationAction
5
+ from infrahub.core.constants import InfrahubKind, MutationAction
6
6
 
7
7
  from .constants import EVENT_NAMESPACE
8
8
  from .models import EventNode, InfrahubEvent
@@ -21,6 +21,11 @@ class GroupMutatedEvent(InfrahubEvent):
21
21
 
22
22
  def get_related(self) -> list[dict[str, str]]:
23
23
  related = super().get_related()
24
+
25
+ if self.kind == InfrahubKind.GRAPHQLQUERYGROUP:
26
+ # Temporary workaround to avoid too large payloads for the related field
27
+ return related
28
+
24
29
  related.append(
25
30
  {
26
31
  "prefect.resource.id": self.node_id,
@@ -7,7 +7,7 @@ from infrahub.core.changelog.models import (
7
7
  RelationshipCardinalityManyChangelog,
8
8
  RelationshipCardinalityOneChangelog,
9
9
  )
10
- from infrahub.core.constants import DiffAction, MutationAction
10
+ from infrahub.core.constants import DiffAction, InfrahubKind, MutationAction
11
11
 
12
12
  from .constants import EVENT_NAMESPACE
13
13
  from .models import InfrahubEvent
@@ -24,6 +24,10 @@ class NodeMutatedEvent(InfrahubEvent):
24
24
 
25
25
  def get_related(self) -> list[dict[str, str]]:
26
26
  related = super().get_related()
27
+ if self.kind == InfrahubKind.GRAPHQLQUERYGROUP:
28
+ # Temporary workaround to avoid too large payloads for the related field
29
+ return related
30
+
27
31
  for attribute in self.changelog.attributes.values():
28
32
  related.append(
29
33
  {
@@ -363,7 +363,7 @@ class InfrahubMutationMixin:
363
363
  branch: Branch,
364
364
  db: InfrahubDatabase,
365
365
  obj: Node,
366
- run_constraint_checks: bool = True,
366
+ skip_uniqueness_check: bool = False,
367
367
  ) -> tuple[Node, Self]:
368
368
  """
369
369
  Wrapper around mutate_update to potentially activate locking and call it within a database transaction.
@@ -378,11 +378,11 @@ class InfrahubMutationMixin:
378
378
  if lock_names:
379
379
  async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
380
380
  obj = await cls.mutate_update_object(
381
- db=db, info=info, data=data, branch=branch, obj=obj, run_constraint_checks=run_constraint_checks
381
+ db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
382
382
  )
383
383
  else:
384
384
  obj = await cls.mutate_update_object(
385
- db=db, info=info, data=data, branch=branch, obj=obj, run_constraint_checks=run_constraint_checks
385
+ db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
386
386
  )
387
387
  result = await cls.mutate_update_to_graphql(db=db, info=info, obj=obj)
388
388
  return obj, result
@@ -396,11 +396,11 @@ class InfrahubMutationMixin:
396
396
  data=data,
397
397
  branch=branch,
398
398
  obj=obj,
399
- run_constraint_checks=run_constraint_checks,
399
+ skip_uniqueness_check=skip_uniqueness_check,
400
400
  )
401
401
  else:
402
402
  obj = await cls.mutate_update_object(
403
- db=dbt, info=info, data=data, branch=branch, obj=obj, run_constraint_checks=run_constraint_checks
403
+ db=dbt, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
404
404
  )
405
405
  result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=obj)
406
406
  return obj, result
@@ -434,7 +434,7 @@ class InfrahubMutationMixin:
434
434
  data: InputObjectType,
435
435
  branch: Branch,
436
436
  obj: Node,
437
- run_constraint_checks: bool = True,
437
+ skip_uniqueness_check: bool = False,
438
438
  ) -> Node:
439
439
  component_registry = get_component_registry()
440
440
  node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
@@ -442,8 +442,9 @@ class InfrahubMutationMixin:
442
442
  before_mutate_profile_ids = await cls._get_profile_ids(db=db, obj=obj)
443
443
  await obj.from_graphql(db=db, data=data)
444
444
  fields_to_validate = list(data)
445
- if run_constraint_checks:
446
- await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
445
+ await node_constraint_runner.check(
446
+ node=obj, field_filters=fields_to_validate, skip_uniqueness_check=skip_uniqueness_check
447
+ )
447
448
 
448
449
  fields = list(data.keys())
449
450
  for field_to_remove in ("id", "hfid"):
@@ -494,7 +495,6 @@ class InfrahubMutationMixin:
494
495
  db = database or graphql_context.db
495
496
  dict_data = dict(data)
496
497
  node = None
497
- run_constraint_checks = True
498
498
 
499
499
  if "id" in dict_data:
500
500
  node = await NodeManager.get_one(
@@ -506,7 +506,6 @@ class InfrahubMutationMixin:
506
506
  db=db,
507
507
  branch=branch,
508
508
  obj=node,
509
- run_constraint_checks=run_constraint_checks,
510
509
  )
511
510
  return updated_obj, mutation, False
512
511
 
@@ -525,7 +524,6 @@ class InfrahubMutationMixin:
525
524
  db=db,
526
525
  branch=branch,
527
526
  obj=node,
528
- run_constraint_checks=run_constraint_checks,
529
527
  )
530
528
  return updated_obj, mutation, False
531
529
 
@@ -545,7 +543,7 @@ class InfrahubMutationMixin:
545
543
  db=db,
546
544
  branch=branch,
547
545
  obj=node,
548
- run_constraint_checks=run_constraint_checks,
546
+ skip_uniqueness_check=True,
549
547
  )
550
548
  return updated_obj, mutation, False
551
549