infrahub-server 1.2.10__py3-none-any.whl → 1.3.0a0__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 (134) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/config.py +9 -0
  10. infrahub/core/account.py +24 -47
  11. infrahub/core/attribute.py +10 -12
  12. infrahub/core/constants/infrahubkind.py +8 -0
  13. infrahub/core/constraint/node/runner.py +1 -1
  14. infrahub/core/convert_object_type/__init__.py +0 -0
  15. infrahub/core/convert_object_type/conversion.py +122 -0
  16. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  17. infrahub/core/diff/query/all_conflicts.py +1 -5
  18. infrahub/core/diff/query/artifact.py +10 -20
  19. infrahub/core/diff/query/diff_get.py +3 -6
  20. infrahub/core/diff/query/field_summary.py +2 -4
  21. infrahub/core/diff/query/merge.py +70 -123
  22. infrahub/core/diff/query/save.py +20 -32
  23. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  24. infrahub/core/diff/query_parser.py +5 -1
  25. infrahub/core/diff/tasks.py +3 -3
  26. infrahub/core/manager.py +14 -11
  27. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  28. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  29. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  30. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  31. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  32. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  33. infrahub/core/migrations/query/attribute_add.py +1 -2
  34. infrahub/core/migrations/query/attribute_rename.py +3 -6
  35. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  36. infrahub/core/migrations/query/node_duplicate.py +3 -6
  37. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  38. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  39. infrahub/core/migrations/schema/node_remove.py +3 -6
  40. infrahub/core/models.py +29 -2
  41. infrahub/core/node/__init__.py +18 -4
  42. infrahub/core/node/create.py +211 -0
  43. infrahub/core/protocols.py +51 -0
  44. infrahub/core/protocols_base.py +3 -0
  45. infrahub/core/query/__init__.py +2 -2
  46. infrahub/core/query/diff.py +26 -32
  47. infrahub/core/query/ipam.py +10 -20
  48. infrahub/core/query/node.py +28 -46
  49. infrahub/core/query/relationship.py +51 -28
  50. infrahub/core/query/resource_manager.py +1 -2
  51. infrahub/core/query/subquery.py +2 -4
  52. infrahub/core/relationship/model.py +3 -0
  53. infrahub/core/schema/__init__.py +2 -1
  54. infrahub/core/schema/attribute_parameters.py +36 -0
  55. infrahub/core/schema/attribute_schema.py +83 -8
  56. infrahub/core/schema/basenode_schema.py +25 -1
  57. infrahub/core/schema/definitions/core/__init__.py +21 -0
  58. infrahub/core/schema/definitions/internal.py +13 -3
  59. infrahub/core/schema/generated/attribute_schema.py +9 -3
  60. infrahub/core/schema/schema_branch.py +12 -7
  61. infrahub/core/validators/__init__.py +5 -1
  62. infrahub/core/validators/attribute/choices.py +1 -2
  63. infrahub/core/validators/attribute/enum.py +1 -2
  64. infrahub/core/validators/attribute/kind.py +1 -2
  65. infrahub/core/validators/attribute/length.py +13 -6
  66. infrahub/core/validators/attribute/optional.py +1 -2
  67. infrahub/core/validators/attribute/regex.py +5 -5
  68. infrahub/core/validators/attribute/unique.py +1 -3
  69. infrahub/core/validators/determiner.py +18 -2
  70. infrahub/core/validators/enum.py +7 -0
  71. infrahub/core/validators/node/hierarchy.py +3 -6
  72. infrahub/core/validators/query.py +1 -3
  73. infrahub/core/validators/relationship/count.py +6 -12
  74. infrahub/core/validators/relationship/optional.py +2 -4
  75. infrahub/core/validators/relationship/peer.py +3 -8
  76. infrahub/core/validators/tasks.py +1 -1
  77. infrahub/core/validators/uniqueness/query.py +5 -9
  78. infrahub/database/__init__.py +1 -3
  79. infrahub/events/group_action.py +1 -0
  80. infrahub/graphql/analyzer.py +139 -18
  81. infrahub/graphql/app.py +1 -1
  82. infrahub/graphql/loaders/node.py +1 -1
  83. infrahub/graphql/loaders/peers.py +1 -1
  84. infrahub/graphql/manager.py +4 -0
  85. infrahub/graphql/mutations/action.py +164 -0
  86. infrahub/graphql/mutations/convert_object_type.py +62 -0
  87. infrahub/graphql/mutations/main.py +24 -175
  88. infrahub/graphql/mutations/proposed_change.py +21 -18
  89. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  90. infrahub/graphql/queries/relationship.py +1 -1
  91. infrahub/graphql/resolvers/many_relationship.py +4 -4
  92. infrahub/graphql/resolvers/resolver.py +4 -4
  93. infrahub/graphql/resolvers/single_relationship.py +2 -2
  94. infrahub/graphql/schema.py +6 -0
  95. infrahub/graphql/subscription/graphql_query.py +2 -2
  96. infrahub/graphql/types/branch.py +1 -1
  97. infrahub/menu/menu.py +31 -0
  98. infrahub/message_bus/messages/__init__.py +0 -10
  99. infrahub/message_bus/operations/__init__.py +0 -8
  100. infrahub/message_bus/operations/refresh/registry.py +1 -1
  101. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  102. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  103. infrahub/prefect_server/models.py +1 -19
  104. infrahub/proposed_change/models.py +68 -3
  105. infrahub/proposed_change/tasks.py +907 -30
  106. infrahub/task_manager/models.py +10 -6
  107. infrahub/telemetry/database.py +1 -1
  108. infrahub/telemetry/tasks.py +1 -1
  109. infrahub/trigger/catalogue.py +2 -0
  110. infrahub/trigger/models.py +18 -2
  111. infrahub/trigger/tasks.py +3 -1
  112. infrahub/workflows/catalogue.py +76 -0
  113. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +2 -2
  114. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +121 -118
  115. infrahub_testcontainers/container.py +0 -1
  116. infrahub_testcontainers/docker-compose.test.yml +1 -1
  117. infrahub_testcontainers/helpers.py +8 -2
  118. infrahub/message_bus/messages/check_generator_run.py +0 -26
  119. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  120. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  121. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  122. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  123. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  124. infrahub/message_bus/operations/check/__init__.py +0 -3
  125. infrahub/message_bus/operations/check/generator.py +0 -156
  126. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  127. infrahub/message_bus/operations/finalize/validator.py +0 -133
  128. infrahub/message_bus/operations/requests/__init__.py +0 -9
  129. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  130. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  131. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  132. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  133. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  134. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -133,8 +133,7 @@ class NodeDuplicateQuery(Query):
133
133
 
134
134
  # ruff: noqa: E501
135
135
  query = """
136
- CALL {
137
- WITH node
136
+ CALL (node) {
138
137
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
139
138
  WHERE %(branch_filter)s
140
139
  RETURN node as n1, r as r1
@@ -147,8 +146,7 @@ class NodeDuplicateQuery(Query):
147
146
  WITH active_node, new_node
148
147
  // Process Outbound Relationship
149
148
  MATCH (active_node)-[]->(peer)
150
- CALL {
151
- WITH active_node, peer
149
+ CALL (active_node, peer) {
152
150
  MATCH (active_node)-[r]->(peer)
153
151
  WHERE %(branch_filter)s
154
152
  RETURN active_node as n1, r as rel_outband1, peer as p1
@@ -167,8 +165,7 @@ class NodeDuplicateQuery(Query):
167
165
  WITH DISTINCT active_node, new_node
168
166
  // Process Inbound Relationship
169
167
  MATCH (active_node)<-[]-(peer)
170
- CALL {
171
- WITH active_node, peer
168
+ CALL (active_node, peer) {
172
169
  MATCH (active_node)<-[r]-(peer)
173
170
  WHERE %(branch_filter)s
174
171
  RETURN active_node as n1, r as rel_inband1, peer as p1
@@ -116,8 +116,7 @@ class RelationshipDuplicateQuery(Query):
116
116
 
117
117
  # ruff: noqa: E501
118
118
  query = """
119
- CALL {
120
- WITH source, rel, destination
119
+ CALL (source, rel, destination) {
121
120
  MATCH path = (source)-[r1:IS_RELATED]-(rel)-[r2:IS_RELATED]-(destination)
122
121
  WHERE all(r IN relationships(path) WHERE %(branch_filter)s)
123
122
  RETURN rel as rel1, r1 as r11, r2 as r12
@@ -130,8 +129,7 @@ class RelationshipDuplicateQuery(Query):
130
129
  WITH DISTINCT(active_rel) as active_rel, new_rel
131
130
  // Process Inbound Relationship
132
131
  MATCH (active_rel)<-[]-(peer)
133
- CALL {
134
- WITH active_rel, peer
132
+ CALL (active_rel, peer) {
135
133
  MATCH (active_rel)<-[r]-(peer)
136
134
  WHERE %(branch_filter)s
137
135
  RETURN active_rel as n1, r as rel_inband1, peer as p1
@@ -150,8 +148,7 @@ class RelationshipDuplicateQuery(Query):
150
148
  WITH DISTINCT(active_rel) as active_rel, new_rel
151
149
  // Process Outbound Relationship
152
150
  MATCH (active_rel)-[]->(peer)
153
- CALL {
154
- WITH active_rel, peer
151
+ CALL (active_rel, peer) {
155
152
  MATCH (active_rel)-[r]->(peer)
156
153
  WHERE %(branch_filter)s
157
154
  RETURN active_rel as n1, r as rel_outband1, peer as p1
@@ -75,8 +75,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
75
75
  MATCH (node:%(node_kind)s)
76
76
  WHERE (size($kinds_to_ignore) = 0 OR NOT any(l IN labels(node) WHERE l IN $kinds_to_ignore))
77
77
  AND exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
78
- CALL {
79
- WITH node
78
+ CALL (node) {
80
79
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
81
80
  WHERE %(branch_filter)s
82
81
  RETURN node as n1, r as r1
@@ -86,8 +85,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
86
85
  WITH n1 as active_node, r1 as rb
87
86
  WHERE rb.status = "active"
88
87
  // Find all the attributes that need to be updated
89
- CALL {
90
- WITH active_node
88
+ CALL (active_node) {
91
89
  MATCH (active_node)-[r:HAS_ATTRIBUTE]-(attr:Attribute { name: $attr_name })
92
90
  WHERE %(branch_filter)s
93
91
  RETURN active_node as n1, r as r1, attr as attr1
@@ -98,8 +96,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
98
96
  WHERE rb.status = "active"
99
97
  WITH active_attr
100
98
  MATCH (active_attr)-[]-(peer)
101
- CALL {
102
- WITH active_attr, peer
99
+ CALL (active_attr, peer) {
103
100
  MATCH (active_attr)-[r]-(peer)
104
101
  WHERE %(branch_filter)s
105
102
  RETURN active_attr as a1, r as r1, peer as p1
@@ -63,8 +63,7 @@ class NodeRemoveMigrationBaseQuery(MigrationQuery):
63
63
  query = """
64
64
  // Find all the active nodes
65
65
  MATCH (node:%(node_kind)s)
66
- CALL {
67
- WITH node
66
+ CALL (node) {
68
67
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
69
68
  WHERE %(branch_filter)s
70
69
  RETURN node as n1, r as r1
@@ -96,8 +95,7 @@ class NodeRemoveMigrationQueryIn(NodeRemoveMigrationBaseQuery):
96
95
  // Process Inbound Relationship
97
96
  WITH active_node
98
97
  MATCH (active_node)<-[]-(peer)
99
- CALL {
100
- WITH active_node, peer
98
+ CALL (active_node, peer) {
101
99
  MATCH (active_node)-[r]->(peer)
102
100
  WHERE %(branch_filter)s
103
101
  RETURN active_node as n1, r as rel_inband1, peer as p1
@@ -142,8 +140,7 @@ class NodeRemoveMigrationQueryOut(NodeRemoveMigrationBaseQuery):
142
140
  // Process Outbound Relationship
143
141
  WITH active_node
144
142
  MATCH (active_node)-[]->(peer)
145
- CALL {
146
- WITH active_node, peer
143
+ CALL (active_node, peer) {
147
144
  MATCH (active_node)-[r]->(peer)
148
145
  WHERE %(branch_filter)s
149
146
  RETURN active_node as n1, r as rel_outband1, peer as p1
infrahub/core/models.py CHANGED
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  from infrahub.core.schema.schema_branch import SchemaBranch
21
21
 
22
22
  GENERIC_ATTRIBUTES_TO_IGNORE = ["namespace", "name", "branch"]
23
+ PROPERTY_NAMES_TO_IGNORE = ["regex", "min_length", "max_length"]
23
24
 
24
25
 
25
26
  class NodeKind(BaseModel):
@@ -252,11 +253,37 @@ class SchemaUpdateValidationResult(BaseModel):
252
253
  if not sub_field_diff:
253
254
  raise ValueError("sub_field_diff must be defined, unexpected situation")
254
255
 
255
- for prop_name in sub_field_diff.changed:
256
+ for prop_name, prop_diff in sub_field_diff.changed.items():
257
+ if prop_name in PROPERTY_NAMES_TO_IGNORE:
258
+ continue
259
+
256
260
  field_info = field.model_fields[prop_name]
257
261
  field_update = str(field_info.json_schema_extra.get("update")) # type: ignore[union-attr]
258
262
 
259
- schema_path = SchemaPath( # type: ignore[call-arg]
263
+ if isinstance(prop_diff, HashableModelDiff):
264
+ for param_field_name in prop_diff.changed:
265
+ # override field_update if this field has its own json_schema_extra.update
266
+ try:
267
+ prop_field = getattr(field, prop_name)
268
+ param_field_info = prop_field.model_fields[param_field_name]
269
+ param_field_update = str(param_field_info.json_schema_extra.get("update"))
270
+ except (AttributeError, KeyError):
271
+ param_field_update = None
272
+
273
+ schema_path = SchemaPath(
274
+ schema_kind=schema.kind,
275
+ path_type=path_type,
276
+ field_name=field_name,
277
+ property_name=f"{prop_name}.{param_field_name}",
278
+ )
279
+
280
+ self._process_field(
281
+ schema_path=schema_path,
282
+ field_update=param_field_update or field_update,
283
+ )
284
+ continue
285
+
286
+ schema_path = SchemaPath(
260
287
  schema_kind=schema.kind,
261
288
  path_type=path_type,
262
289
  field_name=field_name,
@@ -22,7 +22,14 @@ from infrahub.core.constants import (
22
22
  from infrahub.core.constants.schema import SchemaElementPathType
23
23
  from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
24
24
  from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
25
- from infrahub.core.schema import AttributeSchema, NodeSchema, ProfileSchema, RelationshipSchema, TemplateSchema
25
+ from infrahub.core.schema import (
26
+ AttributeSchema,
27
+ NodeSchema,
28
+ NonGenericSchemaTypes,
29
+ ProfileSchema,
30
+ RelationshipSchema,
31
+ TemplateSchema,
32
+ )
26
33
  from infrahub.core.timestamp import Timestamp
27
34
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
28
35
  from infrahub.types import ATTRIBUTE_TYPES
@@ -66,7 +73,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
66
73
  _meta.default_filter = default_filter
67
74
  super().__init_subclass_with_meta__(_meta=_meta, **options)
68
75
 
69
- def get_schema(self) -> NodeSchema | ProfileSchema | TemplateSchema:
76
+ def get_schema(self) -> NonGenericSchemaTypes:
70
77
  return self._schema
71
78
 
72
79
  def get_kind(self) -> str:
@@ -872,9 +879,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
872
879
  if relationship.kind == RelationshipKind.PARENT:
873
880
  return relationship.name
874
881
 
875
- async def get_object_template(self, db: InfrahubDatabase) -> Node | None:
882
+ async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
876
883
  object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
877
- return await object_template.get_peer(db=db) if object_template is not None else None
884
+ return (
885
+ await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
886
+ )
878
887
 
879
888
  def get_relationships(
880
889
  self, kind: RelationshipKind, exclude: Sequence[str] | None = None
@@ -888,3 +897,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
888
897
  for relationship in self.get_schema().relationships
889
898
  if relationship.name not in exclude and relationship.kind == kind
890
899
  ]
900
+
901
+ def validate_relationships(self) -> None:
902
+ for name in self._relationships:
903
+ relm: RelationshipManager = getattr(self, name)
904
+ relm.validate()
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Mapping
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.constants import RelationshipCardinality, RelationshipKind
7
+ from infrahub.core.constraint.node.runner import NodeConstraintRunner
8
+ from infrahub.core.manager import NodeManager
9
+ from infrahub.core.node import Node
10
+ from infrahub.core.protocols import CoreObjectTemplate
11
+ from infrahub.dependencies.registry import get_component_registry
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.core.branch import Branch
15
+ from infrahub.core.relationship.model import RelationshipManager
16
+ from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes, RelationshipSchema
17
+ from infrahub.database import InfrahubDatabase
18
+
19
+
20
+ async def get_template_relationship_peers(
21
+ db: InfrahubDatabase, template: CoreObjectTemplate, relationship: RelationshipSchema
22
+ ) -> Mapping[str, CoreObjectTemplate]:
23
+ """For a given relationship on the template, fetch the related peers."""
24
+ template_relationship_manager: RelationshipManager = getattr(template, relationship.name)
25
+ if relationship.cardinality == RelationshipCardinality.MANY:
26
+ return await template_relationship_manager.get_peers(db=db, peer_type=CoreObjectTemplate)
27
+
28
+ peers: dict[str, CoreObjectTemplate] = {}
29
+ template_relationship_peer = await template_relationship_manager.get_peer(db=db, peer_type=CoreObjectTemplate)
30
+ if template_relationship_peer:
31
+ peers[template_relationship_peer.id] = template_relationship_peer
32
+ return peers
33
+
34
+
35
+ async def extract_peer_data(
36
+ db: InfrahubDatabase,
37
+ template_peer: CoreObjectTemplate,
38
+ obj_peer_schema: MainSchemaTypes,
39
+ parent_obj: Node,
40
+ current_template: CoreObjectTemplate,
41
+ ) -> Mapping[str, Any]:
42
+ obj_peer_data: dict[str, Any] = {}
43
+
44
+ for attr in template_peer.get_schema().attribute_names:
45
+ if attr not in obj_peer_schema.attribute_names:
46
+ continue
47
+ obj_peer_data[attr] = {"value": getattr(template_peer, attr).value, "source": template_peer.id}
48
+
49
+ for rel in template_peer.get_schema().relationship_names:
50
+ rel_manager: RelationshipManager = getattr(template_peer, rel)
51
+ if (
52
+ rel_manager.schema.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
53
+ or rel_manager.schema.name not in obj_peer_schema.relationship_names
54
+ ):
55
+ continue
56
+
57
+ if list(await rel_manager.get_peers(db=db)) == [current_template.id]:
58
+ obj_peer_data[rel] = {"id": parent_obj.id}
59
+
60
+ return obj_peer_data
61
+
62
+
63
+ async def handle_template_relationships(
64
+ db: InfrahubDatabase,
65
+ branch: Branch,
66
+ obj: Node,
67
+ template: CoreObjectTemplate,
68
+ fields: list,
69
+ constraint_runner: NodeConstraintRunner | None = None,
70
+ ) -> None:
71
+ if constraint_runner is None:
72
+ component_registry = get_component_registry()
73
+ constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
74
+
75
+ for relationship in obj.get_relationships(kind=RelationshipKind.COMPONENT, exclude=fields):
76
+ template_relationship_peers = await get_template_relationship_peers(
77
+ db=db, template=template, relationship=relationship
78
+ )
79
+ if not template_relationship_peers:
80
+ continue
81
+
82
+ for template_relationship_peer in template_relationship_peers.values():
83
+ # We retrieve peer schema for each peer in case we are processing a relationship which is based on a generic
84
+ obj_peer_schema = registry.schema.get_node_schema(
85
+ name=template_relationship_peer.get_schema().kind.removeprefix("Template"),
86
+ branch=branch,
87
+ duplicate=False,
88
+ )
89
+ obj_peer_data = await extract_peer_data(
90
+ db=db,
91
+ template_peer=template_relationship_peer,
92
+ obj_peer_schema=obj_peer_schema,
93
+ parent_obj=obj,
94
+ current_template=template,
95
+ )
96
+
97
+ obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch)
98
+ await obj_peer.new(db=db, **obj_peer_data)
99
+ await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
100
+ await obj_peer.save(db=db)
101
+
102
+ await handle_template_relationships(
103
+ db=db,
104
+ branch=branch,
105
+ constraint_runner=constraint_runner,
106
+ obj=obj_peer,
107
+ template=template_relationship_peer,
108
+ fields=fields,
109
+ )
110
+
111
+
112
+ async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
113
+ if not hasattr(obj, "profiles"):
114
+ return set()
115
+ profile_rels = await obj.profiles.get_relationships(db=db)
116
+ return {pr.peer_id for pr in profile_rels}
117
+
118
+
119
+ async def refresh_for_profile_update(
120
+ db: InfrahubDatabase,
121
+ branch: Branch,
122
+ obj: Node,
123
+ schema: NonGenericSchemaTypes,
124
+ previous_profile_ids: set[str] | None = None,
125
+ ) -> Node:
126
+ if not hasattr(obj, "profiles"):
127
+ return obj
128
+ current_profile_ids = await get_profile_ids(db=db, obj=obj)
129
+ if previous_profile_ids is None or previous_profile_ids != current_profile_ids:
130
+ refreshed_node = await NodeManager.get_one_by_id_or_default_filter(
131
+ db=db,
132
+ kind=schema.kind,
133
+ id=obj.get_id(),
134
+ branch=branch,
135
+ include_owner=True,
136
+ include_source=True,
137
+ )
138
+ refreshed_node._node_changelog = obj.node_changelog
139
+ return refreshed_node
140
+ return obj
141
+
142
+
143
+ async def _do_create_node(
144
+ node_class: type[Node],
145
+ db: InfrahubDatabase,
146
+ data: dict,
147
+ schema: NonGenericSchemaTypes,
148
+ fields_to_validate: list,
149
+ branch: Branch,
150
+ node_constraint_runner: NodeConstraintRunner,
151
+ ) -> Node:
152
+ obj = await node_class.init(db=db, schema=schema, branch=branch)
153
+ await obj.new(db=db, **data)
154
+ await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
155
+ await obj.save(db=db)
156
+
157
+ object_template = await obj.get_object_template(db=db)
158
+ if object_template:
159
+ await handle_template_relationships(
160
+ db=db,
161
+ branch=branch,
162
+ template=object_template,
163
+ obj=obj,
164
+ fields=fields_to_validate,
165
+ )
166
+ return obj
167
+
168
+
169
+ async def create_node(
170
+ data: dict,
171
+ db: InfrahubDatabase,
172
+ branch: Branch,
173
+ schema: NonGenericSchemaTypes,
174
+ ) -> Node:
175
+ """Create a node in the database if constraint checks succeed."""
176
+
177
+ component_registry = get_component_registry()
178
+ node_constraint_runner = await component_registry.get_component(
179
+ NodeConstraintRunner, db=db.start_session() if not db.is_transaction else db, branch=branch
180
+ )
181
+ node_class = Node
182
+ if schema.kind in registry.node:
183
+ node_class = registry.node[schema.kind]
184
+
185
+ fields_to_validate = list(data)
186
+ if db.is_transaction:
187
+ obj = await _do_create_node(
188
+ node_class=node_class,
189
+ node_constraint_runner=node_constraint_runner,
190
+ db=db,
191
+ schema=schema,
192
+ branch=branch,
193
+ fields_to_validate=fields_to_validate,
194
+ data=data,
195
+ )
196
+ else:
197
+ async with db.start_transaction() as dbt:
198
+ obj = await _do_create_node(
199
+ node_class=node_class,
200
+ node_constraint_runner=node_constraint_runner,
201
+ db=dbt,
202
+ schema=schema,
203
+ branch=branch,
204
+ fields_to_validate=fields_to_validate,
205
+ data=data,
206
+ )
207
+
208
+ if await get_profile_ids(db=db, obj=obj):
209
+ obj = await refresh_for_profile_update(db=db, branch=branch, schema=schema, obj=obj)
210
+
211
+ return obj
@@ -62,6 +62,12 @@ class BuiltinIPPrefix(CoreNode):
62
62
  children: RelationshipManager
63
63
 
64
64
 
65
+ class CoreAction(CoreNode):
66
+ name: String
67
+ description: StringOptional
68
+ triggers: RelationshipManager
69
+
70
+
65
71
  class CoreArtifactTarget(CoreNode):
66
72
  artifacts: RelationshipManager
67
73
 
@@ -148,6 +154,10 @@ class CoreMenu(CoreNode):
148
154
  children: RelationshipManager
149
155
 
150
156
 
157
+ class CoreNodeTriggerMatch(CoreNode):
158
+ trigger: RelationshipManager
159
+
160
+
151
161
  class CoreObjectComponentTemplate(CoreNode):
152
162
  template_name: String
153
163
 
@@ -189,6 +199,14 @@ class CoreTransformation(CoreNode):
189
199
  tags: RelationshipManager
190
200
 
191
201
 
202
+ class CoreTriggerRule(CoreNode):
203
+ name: String
204
+ description: StringOptional
205
+ active: Boolean
206
+ branch_scope: Dropdown
207
+ action: RelationshipManager
208
+
209
+
192
210
  class CoreValidator(CoreNode):
193
211
  label: StringOptional
194
212
  state: Enum
@@ -322,6 +340,10 @@ class CoreFileThread(CoreThread):
322
340
  repository: RelationshipManager
323
341
 
324
342
 
343
+ class CoreGeneratorAction(CoreAction):
344
+ generator: RelationshipManager
345
+
346
+
325
347
  class CoreGeneratorCheck(CoreCheck):
326
348
  instance: String
327
349
 
@@ -376,6 +398,16 @@ class CoreGraphQLQueryGroup(CoreGroup):
376
398
  query: RelationshipManager
377
399
 
378
400
 
401
+ class CoreGroupAction(CoreAction):
402
+ add_members: Boolean
403
+ group: RelationshipManager
404
+
405
+
406
+ class CoreGroupTriggerRule(CoreTriggerRule):
407
+ members_added: Boolean
408
+ group: RelationshipManager
409
+
410
+
379
411
  class CoreIPAddressPool(CoreResourcePool, LineageSource):
380
412
  default_address_type: String
381
413
  default_prefix_length: IntegerOptional
@@ -395,6 +427,25 @@ class CoreMenuItem(CoreMenu):
395
427
  pass
396
428
 
397
429
 
430
+ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch):
431
+ attribute_name: String
432
+ value: StringOptional
433
+ value_previous: StringOptional
434
+ value_match: Dropdown
435
+
436
+
437
+ class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch):
438
+ relationship_name: String
439
+ added: Boolean
440
+ peer: StringOptional
441
+
442
+
443
+ class CoreNodeTriggerRule(CoreTriggerRule):
444
+ node_kind: String
445
+ mutation_action: Enum
446
+ matches: RelationshipManager
447
+
448
+
398
449
  class CoreNumberPool(CoreResourcePool, LineageSource):
399
450
  node: String
400
451
  node_attribute: String
@@ -7,6 +7,7 @@ from typing_extensions import Self
7
7
  if TYPE_CHECKING:
8
8
  from neo4j import AsyncResult, AsyncSession, AsyncTransaction, Record
9
9
 
10
+ from infrahub.core.schema import NonGenericSchemaTypes
10
11
  from infrahub.core.schema.schema_branch import SchemaBranch
11
12
 
12
13
 
@@ -70,6 +71,8 @@ class CoreNode(Protocol):
70
71
 
71
72
  def get_id(self) -> str: ...
72
73
  def get_kind(self) -> str: ...
74
+ def get_schema(self) -> NonGenericSchemaTypes: ...
75
+
73
76
  @classmethod
74
77
  async def init(
75
78
  cls,
@@ -424,8 +424,8 @@ class Query(ABC):
424
424
  else:
425
425
  self.query_lines.extend([line.strip() for line in query.split("\n") if line.strip()])
426
426
 
427
- def add_subquery(self, subquery: str, with_clause: str | None = None) -> None:
428
- self.add_to_query("CALL {")
427
+ def add_subquery(self, subquery: str, node_alias: str, with_clause: str | None = None) -> None:
428
+ self.add_to_query(f"CALL ({node_alias}) {{")
429
429
  self.add_to_query(subquery)
430
430
  self.add_to_query("}")
431
431
  if with_clause: