infrahub-server 1.3.5__py3-none-any.whl → 1.4.0b0__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 (158) hide show
  1. infrahub/api/internal.py +5 -0
  2. infrahub/artifacts/tasks.py +17 -22
  3. infrahub/branch/merge_mutation_checker.py +38 -0
  4. infrahub/cli/__init__.py +2 -2
  5. infrahub/cli/context.py +7 -3
  6. infrahub/cli/db.py +5 -16
  7. infrahub/cli/upgrade.py +7 -29
  8. infrahub/computed_attribute/tasks.py +36 -46
  9. infrahub/config.py +53 -2
  10. infrahub/constants/environment.py +1 -0
  11. infrahub/core/attribute.py +9 -7
  12. infrahub/core/branch/tasks.py +43 -41
  13. infrahub/core/constants/__init__.py +20 -6
  14. infrahub/core/constants/infrahubkind.py +2 -0
  15. infrahub/core/diff/coordinator.py +3 -1
  16. infrahub/core/diff/repository/repository.py +0 -8
  17. infrahub/core/diff/tasks.py +11 -8
  18. infrahub/core/graph/__init__.py +1 -1
  19. infrahub/core/graph/index.py +1 -2
  20. infrahub/core/graph/schema.py +50 -29
  21. infrahub/core/initialization.py +62 -33
  22. infrahub/core/ipam/tasks.py +4 -3
  23. infrahub/core/merge.py +8 -10
  24. infrahub/core/migrations/graph/__init__.py +2 -0
  25. infrahub/core/migrations/graph/m035_drop_attr_value_index.py +45 -0
  26. infrahub/core/migrations/query/attribute_add.py +27 -2
  27. infrahub/core/migrations/schema/tasks.py +6 -5
  28. infrahub/core/node/proposed_change.py +43 -0
  29. infrahub/core/protocols.py +12 -0
  30. infrahub/core/query/attribute.py +32 -14
  31. infrahub/core/query/diff.py +11 -0
  32. infrahub/core/query/ipam.py +13 -7
  33. infrahub/core/query/node.py +51 -10
  34. infrahub/core/query/resource_manager.py +3 -3
  35. infrahub/core/schema/basenode_schema.py +8 -0
  36. infrahub/core/schema/definitions/core/__init__.py +10 -1
  37. infrahub/core/schema/definitions/core/ipam.py +28 -2
  38. infrahub/core/schema/definitions/core/propose_change.py +15 -0
  39. infrahub/core/schema/definitions/core/webhook.py +3 -0
  40. infrahub/core/schema/generic_schema.py +10 -0
  41. infrahub/core/schema/manager.py +10 -1
  42. infrahub/core/schema/node_schema.py +22 -17
  43. infrahub/core/schema/profile_schema.py +8 -0
  44. infrahub/core/schema/schema_branch.py +9 -5
  45. infrahub/core/schema/template_schema.py +8 -0
  46. infrahub/core/validators/checks_runner.py +5 -5
  47. infrahub/core/validators/tasks.py +6 -7
  48. infrahub/core/validators/uniqueness/checker.py +4 -2
  49. infrahub/core/validators/uniqueness/model.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +57 -7
  51. infrahub/database/__init__.py +2 -1
  52. infrahub/events/__init__.py +18 -0
  53. infrahub/events/constants.py +7 -0
  54. infrahub/events/generator.py +29 -2
  55. infrahub/events/proposed_change_action.py +181 -0
  56. infrahub/generators/tasks.py +24 -20
  57. infrahub/git/base.py +4 -7
  58. infrahub/git/integrator.py +21 -12
  59. infrahub/git/repository.py +15 -30
  60. infrahub/git/tasks.py +121 -106
  61. infrahub/graphql/field_extractor.py +69 -0
  62. infrahub/graphql/manager.py +15 -11
  63. infrahub/graphql/mutations/account.py +2 -2
  64. infrahub/graphql/mutations/action.py +8 -2
  65. infrahub/graphql/mutations/artifact_definition.py +4 -1
  66. infrahub/graphql/mutations/branch.py +10 -5
  67. infrahub/graphql/mutations/graphql_query.py +2 -1
  68. infrahub/graphql/mutations/main.py +14 -8
  69. infrahub/graphql/mutations/menu.py +2 -1
  70. infrahub/graphql/mutations/proposed_change.py +225 -8
  71. infrahub/graphql/mutations/relationship.py +5 -0
  72. infrahub/graphql/mutations/repository.py +2 -1
  73. infrahub/graphql/mutations/tasks.py +7 -9
  74. infrahub/graphql/mutations/webhook.py +4 -1
  75. infrahub/graphql/parser.py +15 -6
  76. infrahub/graphql/queries/__init__.py +10 -1
  77. infrahub/graphql/queries/account.py +3 -3
  78. infrahub/graphql/queries/branch.py +2 -2
  79. infrahub/graphql/queries/diff/tree.py +3 -3
  80. infrahub/graphql/queries/event.py +13 -3
  81. infrahub/graphql/queries/ipam.py +23 -1
  82. infrahub/graphql/queries/proposed_change.py +84 -0
  83. infrahub/graphql/queries/relationship.py +2 -2
  84. infrahub/graphql/queries/resource_manager.py +3 -3
  85. infrahub/graphql/queries/search.py +3 -2
  86. infrahub/graphql/queries/status.py +3 -2
  87. infrahub/graphql/queries/task.py +2 -2
  88. infrahub/graphql/resolvers/ipam.py +440 -0
  89. infrahub/graphql/resolvers/many_relationship.py +4 -3
  90. infrahub/graphql/resolvers/resolver.py +5 -5
  91. infrahub/graphql/resolvers/single_relationship.py +3 -2
  92. infrahub/graphql/schema.py +25 -5
  93. infrahub/graphql/types/__init__.py +2 -2
  94. infrahub/graphql/types/attribute.py +3 -3
  95. infrahub/graphql/types/event.py +60 -0
  96. infrahub/groups/tasks.py +6 -6
  97. infrahub/lock.py +3 -2
  98. infrahub/menu/generator.py +8 -0
  99. infrahub/message_bus/operations/__init__.py +9 -12
  100. infrahub/message_bus/operations/git/file.py +6 -5
  101. infrahub/message_bus/operations/git/repository.py +12 -20
  102. infrahub/message_bus/operations/refresh/registry.py +15 -9
  103. infrahub/message_bus/operations/send/echo.py +7 -4
  104. infrahub/message_bus/types.py +1 -0
  105. infrahub/permissions/globals.py +1 -4
  106. infrahub/permissions/manager.py +8 -5
  107. infrahub/pools/prefix.py +7 -5
  108. infrahub/prefect_server/app.py +31 -0
  109. infrahub/prefect_server/bootstrap.py +18 -0
  110. infrahub/proposed_change/action_checker.py +206 -0
  111. infrahub/proposed_change/approval_revoker.py +40 -0
  112. infrahub/proposed_change/branch_diff.py +3 -1
  113. infrahub/proposed_change/checker.py +45 -0
  114. infrahub/proposed_change/constants.py +32 -2
  115. infrahub/proposed_change/tasks.py +182 -150
  116. infrahub/py.typed +0 -0
  117. infrahub/server.py +29 -17
  118. infrahub/services/__init__.py +13 -28
  119. infrahub/services/adapters/cache/__init__.py +4 -0
  120. infrahub/services/adapters/cache/nats.py +2 -0
  121. infrahub/services/adapters/cache/redis.py +3 -0
  122. infrahub/services/adapters/message_bus/__init__.py +0 -2
  123. infrahub/services/adapters/message_bus/local.py +1 -2
  124. infrahub/services/adapters/message_bus/nats.py +6 -8
  125. infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
  126. infrahub/services/adapters/workflow/__init__.py +1 -0
  127. infrahub/services/adapters/workflow/local.py +1 -8
  128. infrahub/services/component.py +2 -1
  129. infrahub/task_manager/event.py +52 -0
  130. infrahub/task_manager/models.py +9 -0
  131. infrahub/tasks/artifact.py +6 -7
  132. infrahub/tasks/check.py +4 -7
  133. infrahub/telemetry/tasks.py +15 -18
  134. infrahub/transformations/tasks.py +10 -6
  135. infrahub/trigger/tasks.py +4 -3
  136. infrahub/types.py +4 -0
  137. infrahub/validators/events.py +7 -7
  138. infrahub/validators/tasks.py +6 -7
  139. infrahub/webhook/models.py +45 -45
  140. infrahub/webhook/tasks.py +25 -24
  141. infrahub/workers/dependencies.py +143 -0
  142. infrahub/workers/infrahub_async.py +19 -43
  143. infrahub/workflows/catalogue.py +16 -2
  144. infrahub/workflows/initialization.py +5 -4
  145. infrahub/workflows/models.py +2 -0
  146. infrahub_sdk/client.py +6 -6
  147. infrahub_sdk/ctl/repository.py +51 -0
  148. infrahub_sdk/ctl/schema.py +9 -9
  149. infrahub_sdk/protocols.py +40 -6
  150. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/METADATA +5 -4
  151. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/RECORD +158 -144
  152. infrahub_testcontainers/container.py +17 -0
  153. infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
  154. infrahub_testcontainers/docker-compose.test.yml +56 -1
  155. infrahub_testcontainers/helpers.py +4 -1
  156. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/LICENSE.txt +0 -0
  157. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/WHEEL +0 -0
  158. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/entry_points.txt +0 -0
@@ -119,7 +119,7 @@ class SchemaBranch:
119
119
  def template_names(self) -> list[str]:
120
120
  return list(self.templates.keys())
121
121
 
122
- def get_all_kind_id_map(self, nodes_and_generics_only: bool = False) -> dict[str, str]:
122
+ def get_all_kind_id_map(self, nodes_and_generics_only: bool = False) -> dict[str, str | None]:
123
123
  kind_id_map = {}
124
124
  if nodes_and_generics_only:
125
125
  names = self.node_names + self.generic_names_without_templates
@@ -441,7 +441,7 @@ class SchemaBranch:
441
441
  return list(all_schemas.values())
442
442
 
443
443
  def get_schemas_by_rel_identifier(self, identifier: str) -> list[MainSchemaTypes]:
444
- nodes: list[RelationshipSchema] = []
444
+ nodes: list[MainSchemaTypes] = []
445
445
  for node_name in list(self.nodes.keys()) + list(self.generics.keys()):
446
446
  node = self.get(name=node_name, duplicate=False)
447
447
  rel = node.get_relationship_by_identifier(id=identifier, raise_on_error=False)
@@ -660,7 +660,7 @@ class SchemaBranch:
660
660
  and not (
661
661
  schema_attribute_path.relationship_schema.name == "ip_namespace"
662
662
  and isinstance(node_schema, NodeSchema)
663
- and (node_schema.is_ip_address() or node_schema.is_ip_prefix())
663
+ and (node_schema.is_ip_address or node_schema.is_ip_prefix)
664
664
  )
665
665
  ):
666
666
  raise ValueError(
@@ -1447,7 +1447,8 @@ class SchemaBranch:
1447
1447
  node.validate_inheritance(interface=generic_kind_schema)
1448
1448
 
1449
1449
  # Store the list of node referencing a specific generics
1450
- generics_used_by[generic_kind].append(node.kind)
1450
+ if node.namespace != "Internal":
1451
+ generics_used_by[generic_kind].append(node.kind)
1451
1452
  node.inherit_from_interface(interface=generic_kind_schema)
1452
1453
 
1453
1454
  if len(generic_with_hierarchical_support) > 1:
@@ -1925,7 +1926,10 @@ class SchemaBranch:
1925
1926
  for node_name in self.node_names + self.generic_names:
1926
1927
  node = self.get(name=node_name, duplicate=False)
1927
1928
 
1928
- if node.namespace in RESTRICTED_NAMESPACES:
1929
+ if node.namespace in RESTRICTED_NAMESPACES and node.kind not in (
1930
+ InfrahubKind.IPRANGEAVAILABLE,
1931
+ InfrahubKind.IPPREFIXAVAILABLE,
1932
+ ):
1929
1933
  continue
1930
1934
 
1931
1935
  profiles_rel_settings: dict[str, Any] = {
@@ -27,6 +27,14 @@ class TemplateSchema(BaseNodeSchema):
27
27
  def is_template_schema(self) -> bool:
28
28
  return True
29
29
 
30
+ @property
31
+ def is_ip_prefix(self) -> bool:
32
+ return False
33
+
34
+ @property
35
+ def is_ip_address(self) -> bool:
36
+ return False
37
+
30
38
  def get_labels(self) -> list[str]:
31
39
  """Return the labels for this object, composed of the kind and the list of Generic this object is inheriting from."""
32
40
 
@@ -6,7 +6,7 @@ from infrahub_sdk.protocols import CoreValidator
6
6
  from infrahub.context import InfrahubContext
7
7
  from infrahub.core.constants import ValidatorConclusion, ValidatorState
8
8
  from infrahub.core.timestamp import Timestamp
9
- from infrahub.services import InfrahubServices
9
+ from infrahub.services.adapters.event import InfrahubEventService
10
10
  from infrahub.validators.events import send_failed_validator, send_passed_validator
11
11
 
12
12
 
@@ -14,7 +14,7 @@ async def run_checks_and_update_validator(
14
14
  checks: list[Coroutine[Any, None, ValidatorConclusion]],
15
15
  validator: CoreValidator,
16
16
  context: InfrahubContext,
17
- service: InfrahubServices,
17
+ event_service: InfrahubEventService,
18
18
  proposed_change_id: str,
19
19
  ) -> None:
20
20
  """
@@ -38,7 +38,7 @@ async def run_checks_and_update_validator(
38
38
  failed_early = True
39
39
  await validator.save()
40
40
  await send_failed_validator(
41
- service=service, validator=validator, proposed_change_id=proposed_change_id, context=context
41
+ event_service=event_service, validator=validator, proposed_change_id=proposed_change_id, context=context
42
42
  )
43
43
  # Continue to iterate to wait for the end of all checks
44
44
 
@@ -52,9 +52,9 @@ async def run_checks_and_update_validator(
52
52
  if not failed_early:
53
53
  if validator.conclusion.value == ValidatorConclusion.SUCCESS.value:
54
54
  await send_passed_validator(
55
- service=service, validator=validator, proposed_change_id=proposed_change_id, context=context
55
+ event_service=event_service, validator=validator, proposed_change_id=proposed_change_id, context=context
56
56
  )
57
57
  else:
58
58
  await send_failed_validator(
59
- service=service, validator=validator, proposed_change_id=proposed_change_id, context=context
59
+ event_service=event_service, validator=validator, proposed_change_id=proposed_change_id, context=context
60
60
  )
@@ -13,19 +13,18 @@ from infrahub.core.schema import GenericSchema, NodeSchema
13
13
  from infrahub.core.validators.aggregated_checker import AggregatedConstraintChecker
14
14
  from infrahub.core.validators.model import SchemaConstraintValidatorRequest, SchemaViolation
15
15
  from infrahub.dependencies.registry import get_component_registry
16
- from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
16
+ from infrahub.workers.dependencies import get_database
17
17
  from infrahub.workflows.utils import add_tags
18
18
 
19
19
  from .models.validate_migration import SchemaValidateMigrationData, SchemaValidatorPathResponseData
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from infrahub.core.schema.schema_branch import SchemaBranch
23
+ from infrahub.database import InfrahubDatabase
23
24
 
24
25
 
25
26
  @flow(name="schema_validate_migrations", flow_run_name="Validate schema migrations", persist_result=True)
26
- async def schema_validate_migrations(
27
- message: SchemaValidateMigrationData, service: InfrahubServices
28
- ) -> list[SchemaValidatorPathResponseData]:
27
+ async def schema_validate_migrations(message: SchemaValidateMigrationData) -> list[SchemaValidatorPathResponseData]:
29
28
  batch = InfrahubBatch(return_exceptions=True)
30
29
  log = get_run_logger()
31
30
  await add_tags(branches=[message.branch.name])
@@ -47,7 +46,7 @@ async def schema_validate_migrations(
47
46
  node_schema=schema,
48
47
  schema_path=constraint.path,
49
48
  schema_branch=message.schema_branch,
50
- service=service,
49
+ database=await get_database(),
51
50
  )
52
51
 
53
52
  results = [result async for _, result in batch.execute()]
@@ -67,9 +66,9 @@ async def schema_path_validate(
67
66
  node_schema: NodeSchema | GenericSchema,
68
67
  schema_path: SchemaPath,
69
68
  schema_branch: SchemaBranch,
70
- service: InfrahubServices,
69
+ database: InfrahubDatabase,
71
70
  ) -> SchemaValidatorPathResponseData:
72
- async with service.database.start_session(read_only=True) as db:
71
+ async with database.start_session(read_only=True) as db:
73
72
  constraint_request = SchemaConstraintValidatorRequest(
74
73
  branch=branch,
75
74
  constraint_name=constraint_name,
@@ -75,7 +75,7 @@ class UniquenessChecker(ConstraintCheckerInterface):
75
75
 
76
76
  async def build_query_request(self, schema: MainSchemaTypes) -> NodeUniquenessQueryRequest:
77
77
  unique_attr_paths = {
78
- QueryAttributePath(attribute_name=attr_schema.name, property_name="value")
78
+ QueryAttributePath(attribute_name=attr_schema.name, attribute_kind=attr_schema.kind, property_name="value")
79
79
  for attr_schema in schema.unique_attributes
80
80
  }
81
81
  relationship_attr_paths = set()
@@ -92,7 +92,9 @@ class UniquenessChecker(ConstraintCheckerInterface):
92
92
  sub_schema, property_name = get_attribute_path_from_string(path, schema)
93
93
  if isinstance(sub_schema, AttributeSchema):
94
94
  unique_attr_paths.add(
95
- QueryAttributePath(attribute_name=sub_schema.name, property_name=property_name)
95
+ QueryAttributePath(
96
+ attribute_name=sub_schema.name, attribute_kind=sub_schema.kind, property_name=property_name
97
+ )
96
98
  )
97
99
  elif isinstance(sub_schema, RelationshipSchema):
98
100
  relationship_attr_paths.add(
@@ -22,6 +22,7 @@ class QueryRelationshipAttributePath(BaseModel):
22
22
 
23
23
  class QueryAttributePath(BaseModel):
24
24
  attribute_name: str
25
+ attribute_kind: str
25
26
  property_name: str | None = Field(default=None)
26
27
  value: Any | None = Field(default=None)
27
28
 
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from infrahub.core.constants.relationship_label import RELATIONSHIP_TO_VALUE_LABEL
6
+ from infrahub.core.graph.schema import GraphAttributeValueIndexedNode, GraphAttributeValueNode
6
7
  from infrahub.core.query import Query, QueryType
8
+ from infrahub.types import is_large_attribute_type
7
9
 
8
10
  from .model import QueryAttributePathValued, QueryRelationshipPathValued
9
11
 
@@ -40,6 +42,7 @@ class NodeUniqueAttributeConstraintQuery(Query):
40
42
  items="relationships(active_path)", item_names=["branch", "branch_level"]
41
43
  )
42
44
 
45
+ attrs_include_large_type = False
43
46
  attribute_names = set()
44
47
  attr_paths, attr_paths_with_value, attr_values = [], [], []
45
48
  for attr_path in self.query_request.unique_attribute_paths:
@@ -49,12 +52,19 @@ class NodeUniqueAttributeConstraintQuery(Query):
49
52
  raise ValueError(
50
53
  f"{attr_path.property_name} is not a valid property for a uniqueness constraint"
51
54
  ) from exc
55
+ if is_large_attribute_type(attr_path.attribute_kind):
56
+ attrs_include_large_type = True
52
57
  attribute_names.add(attr_path.attribute_name)
53
58
  if attr_path.value:
54
59
  attr_paths_with_value.append((attr_path.attribute_name, property_rel_name, attr_path.value))
55
60
  attr_values.append(attr_path.value)
56
61
  else:
57
62
  attr_paths.append((attr_path.attribute_name, property_rel_name))
63
+ attr_value_label = (
64
+ GraphAttributeValueNode.get_default_label()
65
+ if attrs_include_large_type
66
+ else GraphAttributeValueIndexedNode.get_default_label()
67
+ )
58
68
 
59
69
  relationship_names = set()
60
70
  relationship_attr_paths = []
@@ -112,11 +122,11 @@ class NodeUniqueAttributeConstraintQuery(Query):
112
122
  """ % {"node_kind": self.query_request.kind}
113
123
 
114
124
  attr_paths_with_value_subquery = """
115
- MATCH attr_path = (start_node:%(node_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)-[r:HAS_VALUE]->(attr_value:AttributeValue)
125
+ MATCH attr_path = (start_node:%(node_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)-[r:HAS_VALUE]->(attr_value:%(attr_value_label)s)
116
126
  WHERE attr.name in $attribute_names AND attr_value.value in $attr_values
117
127
  AND [attr.name, type(r), attr_value.value] in $attr_paths_with_value
118
128
  RETURN start_node, attr_path as potential_path, NULL as rel_identifier, attr.name as potential_attr, attr_value.value as potential_attr_value
119
- """ % {"node_kind": self.query_request.kind}
129
+ """ % {"node_kind": self.query_request.kind, "attr_value_label": attr_value_label}
120
130
 
121
131
  relationship_attr_paths_subquery = """
122
132
  MATCH rel_path = (start_node:%(node_kind)s)-[:IS_RELATED]-(relationship_node:Relationship)-[:IS_RELATED]-(related_n:Node)-[:HAS_ATTRIBUTE]->(rel_attr:Attribute)-[:HAS_VALUE]->(rel_attr_value:AttributeValue)
@@ -262,8 +272,20 @@ class UniquenessValidationQuery(Query):
262
272
  self.node_ids_to_exclude = node_ids_to_exclude
263
273
  super().__init__(**kwargs)
264
274
 
275
+ def _is_attribute_large_type(self, db: InfrahubDatabase, node_kind: str, attribute_name: str) -> bool:
276
+ """Determine if an attribute is a large type that should use AttributeValue instead of AttributeValueIndexed."""
277
+ node_schema = db.schema.get(node_kind, branch=self.branch, duplicate=False)
278
+ attr_schema = node_schema.get_attribute(attribute_name)
279
+ return is_large_attribute_type(attr_schema.kind)
280
+
265
281
  def _build_attr_subquery(
266
- self, node_kind: str, attr_path: QueryAttributePathValued, index: int, branch_filter: str, is_first_query: bool
282
+ self,
283
+ node_kind: str,
284
+ attr_path: QueryAttributePathValued,
285
+ index: int,
286
+ branch_filter: str,
287
+ is_first_query: bool,
288
+ is_large_type: bool,
267
289
  ) -> tuple[str, dict[str, str | int | float | bool]]:
268
290
  attr_name_var = f"attr_name_{index}"
269
291
  attr_value_var = f"attr_value_{index}"
@@ -271,8 +293,16 @@ class UniquenessValidationQuery(Query):
271
293
  first_query_filter = "WHERE $node_ids_to_exclude IS NULL OR NOT node.uuid IN $node_ids_to_exclude"
272
294
  else:
273
295
  first_query_filter = ""
296
+
297
+ # Determine the appropriate label based on attribute type
298
+ attr_value_label = (
299
+ GraphAttributeValueNode.get_default_label()
300
+ if is_large_type
301
+ else GraphAttributeValueIndexedNode.get_default_label()
302
+ )
303
+
274
304
  attribute_query = """
275
- MATCH (node:%(node_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})-[:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})
305
+ MATCH (node:%(node_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})-[:HAS_VALUE]->(:%(attr_value_label)s {value: $%(attr_value_var)s})
276
306
  %(first_query_filter)s
277
307
  WITH DISTINCT node
278
308
  CALL (node) {
@@ -300,6 +330,7 @@ CALL (node) {
300
330
  "attr_value_var": attr_value_var,
301
331
  "branch_filter": branch_filter,
302
332
  "index": index,
333
+ "attr_value_label": attr_value_label,
303
334
  }
304
335
  params: dict[str, str | int | float | bool] = {
305
336
  attr_name_var: attr_path.attribute_name,
@@ -314,6 +345,7 @@ CALL (node) {
314
345
  index: int,
315
346
  branch_filter: str,
316
347
  is_first_query: bool,
348
+ is_large_type: bool = False,
317
349
  ) -> tuple[str, dict[str, str | int | float | bool]]:
318
350
  params: dict[str, str | int | float | bool] = {}
319
351
  rel_attr_query = ""
@@ -321,6 +353,14 @@ CALL (node) {
321
353
  if rel_path.attribute_name and rel_path.attribute_value:
322
354
  attr_name_var = f"attr_name_{index}"
323
355
  attr_value_var = f"attr_value_{index}"
356
+
357
+ # Determine the appropriate label based on relationship attribute type
358
+ rel_attr_value_label = (
359
+ GraphAttributeValueNode.get_default_label()
360
+ if is_large_type
361
+ else GraphAttributeValueIndexedNode.get_default_label()
362
+ )
363
+
324
364
  rel_attr_query = """
325
365
  MATCH (peer)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})
326
366
  WHERE %(branch_filter)s
@@ -330,19 +370,25 @@ CALL (node) {
330
370
  LIMIT 1
331
371
  WITH attr, is_active
332
372
  WHERE is_active = TRUE
333
- MATCH (attr)-[r:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})
373
+ MATCH (attr)-[r:HAS_VALUE]->(:%(rel_attr_value_label)s {value: $%(attr_value_var)s})
334
374
  WHERE %(branch_filter)s
335
375
  WITH r
336
376
  ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
337
377
  LIMIT 1
338
378
  WITH r
339
379
  WHERE r.status = "active"
340
- """ % {"attr_name_var": attr_name_var, "attr_value_var": attr_value_var, "branch_filter": branch_filter}
380
+ """ % {
381
+ "attr_name_var": attr_name_var,
382
+ "attr_value_var": attr_value_var,
383
+ "branch_filter": branch_filter,
384
+ "rel_attr_value_label": rel_attr_value_label,
385
+ }
341
386
  rel_attr_match = (
342
- "-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})-[:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})"
387
+ "-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})-[:HAS_VALUE]->(:%(rel_attr_value_label)s {value: $%(attr_value_var)s})"
343
388
  % {
344
389
  "attr_name_var": attr_name_var,
345
390
  "attr_value_var": attr_value_var,
391
+ "rel_attr_value_label": rel_attr_value_label,
346
392
  }
347
393
  )
348
394
  params[attr_name_var] = rel_path.attribute_name
@@ -426,12 +472,16 @@ CALL (node) {
426
472
  for index, schema_path in enumerate(self.query_request.unique_valued_paths):
427
473
  is_first_query = index == 0
428
474
  if isinstance(schema_path, QueryAttributePathValued):
475
+ is_large_type = self._is_attribute_large_type(
476
+ db=db, node_kind=self.query_request.kind, attribute_name=schema_path.attribute_name
477
+ )
429
478
  subquery, params = self._build_attr_subquery(
430
479
  node_kind=self.query_request.kind,
431
480
  attr_path=schema_path,
432
481
  index=index,
433
482
  branch_filter=branch_filter,
434
483
  is_first_query=is_first_query,
484
+ is_large_type=is_large_type,
435
485
  )
436
486
  else:
437
487
  subquery, params = self._build_rel_subquery(
@@ -295,7 +295,8 @@ class InfrahubDatabase:
295
295
  traceback: TracebackType | None,
296
296
  ) -> None:
297
297
  if self._mode == InfrahubDatabaseMode.SESSION:
298
- return await self._session.close()
298
+ await self._session.close()
299
+ return
299
300
 
300
301
  if self._mode == InfrahubDatabaseMode.TRANSACTION:
301
302
  if exc_type is not None:
@@ -3,6 +3,16 @@ from .branch_action import BranchCreatedEvent, BranchDeletedEvent, BranchMergedE
3
3
  from .group_action import GroupMemberAddedEvent, GroupMemberRemovedEvent
4
4
  from .models import EventMeta, InfrahubEvent
5
5
  from .node_action import NodeCreatedEvent, NodeDeletedEvent, NodeUpdatedEvent
6
+ from .proposed_change_action import (
7
+ ProposedChangeApprovalRevokedEvent,
8
+ ProposedChangeApprovedEvent,
9
+ ProposedChangeMergedEvent,
10
+ ProposedChangeRejectedEvent,
11
+ ProposedChangeRejectionRevokedEvent,
12
+ ProposedChangeReviewRequestedEvent,
13
+ ProposedChangeThreadCreatedEvent,
14
+ ProposedChangeThreadUpdatedEvent,
15
+ )
6
16
  from .repository_action import CommitUpdatedEvent
7
17
  from .validator_action import ValidatorFailedEvent, ValidatorPassedEvent, ValidatorStartedEvent
8
18
 
@@ -21,6 +31,14 @@ __all__ = [
21
31
  "NodeCreatedEvent",
22
32
  "NodeDeletedEvent",
23
33
  "NodeUpdatedEvent",
34
+ "ProposedChangeApprovalRevokedEvent",
35
+ "ProposedChangeApprovedEvent",
36
+ "ProposedChangeMergedEvent",
37
+ "ProposedChangeRejectedEvent",
38
+ "ProposedChangeRejectionRevokedEvent",
39
+ "ProposedChangeReviewRequestedEvent",
40
+ "ProposedChangeThreadCreatedEvent",
41
+ "ProposedChangeThreadUpdatedEvent",
24
42
  "ValidatorFailedEvent",
25
43
  "ValidatorPassedEvent",
26
44
  "ValidatorStartedEvent",
@@ -1 +1,8 @@
1
+ from enum import Enum
2
+
1
3
  EVENT_NAMESPACE = "infrahub"
4
+
5
+
6
+ class EventSortOrder(str, Enum):
7
+ ASC = "asc"
8
+ DESC = "desc"
@@ -1,14 +1,16 @@
1
1
  from infrahub.context import InfrahubContext
2
2
  from infrahub.core.branch import Branch
3
3
  from infrahub.core.changelog.models import RelationshipChangelogGetter
4
- from infrahub.core.constants import MutationAction
4
+ from infrahub.core.constants import InfrahubKind, MutationAction
5
5
  from infrahub.core.node import Node
6
+ from infrahub.core.protocols import CoreProposedChange
6
7
  from infrahub.database import InfrahubDatabase
7
8
  from infrahub.events.node_action import NodeDeletedEvent, NodeMutatedEvent, NodeUpdatedEvent, get_node_event
8
9
  from infrahub.groups.parsers import GroupNodeMutationParser
9
10
  from infrahub.worker import WORKER_IDENTITY
10
11
 
11
12
  from .models import EventMeta, InfrahubEvent
13
+ from .proposed_change_action import ProposedChangeThreadCreatedEvent, ProposedChangeThreadUpdatedEvent
12
14
 
13
15
 
14
16
  async def generate_node_mutation_events(
@@ -68,4 +70,29 @@ async def generate_node_mutation_events(
68
70
 
69
71
  group_parser = GroupNodeMutationParser(db=db, branch=branch)
70
72
  group_events = await group_parser.group_events_from_node_actions(events=events)
71
- return events + group_events
73
+
74
+ specific_events: list[InfrahubEvent] = []
75
+ if (kind := node.get_kind()) in [
76
+ InfrahubKind.CHANGETHREAD,
77
+ InfrahubKind.OBJECTTHREAD,
78
+ InfrahubKind.ARTIFACTTHREAD,
79
+ InfrahubKind.FILETHREAD,
80
+ ]:
81
+ proposed_change: CoreProposedChange = await node.change.get_peer(db=db, peer_type=CoreProposedChange) # type: ignore[attr-defined]
82
+ action_to_event_map = {
83
+ MutationAction.CREATED: ProposedChangeThreadCreatedEvent,
84
+ MutationAction.UPDATED: ProposedChangeThreadUpdatedEvent,
85
+ }
86
+ if action in action_to_event_map:
87
+ specific_events.append(
88
+ action_to_event_map[action](
89
+ proposed_change_id=proposed_change.id,
90
+ proposed_change_name=proposed_change.name.value,
91
+ proposed_change_state=proposed_change.state.value,
92
+ thread_id=node.id,
93
+ thread_kind=kind,
94
+ meta=EventMeta.from_context(context=context),
95
+ )
96
+ )
97
+
98
+ return events + group_events + specific_events
@@ -0,0 +1,181 @@
1
+ from typing import ClassVar
2
+
3
+ from pydantic import Field
4
+
5
+ from infrahub.core.constants import InfrahubKind, MutationAction
6
+
7
+ from .constants import EVENT_NAMESPACE
8
+ from .models import InfrahubEvent
9
+
10
+
11
+ class ProposedChangeEvent(InfrahubEvent):
12
+ proposed_change_id: str = Field(..., description="The ID of the proposed change")
13
+ proposed_change_name: str = Field(..., description="The name of the proposed change")
14
+ proposed_change_state: str = Field(..., description="The state of the proposed change")
15
+
16
+ def get_resource(self) -> dict[str, str]:
17
+ return {
18
+ "prefect.resource.id": f"infrahub.proposed_change.{self.proposed_change_id}",
19
+ "infrahub.node.kind": InfrahubKind.PROPOSEDCHANGE,
20
+ "infrahub.node.id": self.proposed_change_id,
21
+ "infrahub.proposed_change.name": self.proposed_change_name,
22
+ "infrahub.proposed_change.state": self.proposed_change_state,
23
+ "infrahub.branch.name": self.meta.context.branch.name,
24
+ }
25
+
26
+
27
+ class ProposedChangeReviewEvent(ProposedChangeEvent):
28
+ reviewer_account_id: str = Field(..., description="The ID of the user who reviewed the proposed change")
29
+ reviewer_account_name: str = Field(..., description="The name of the user who reviewed the proposed change")
30
+ reviewer_decision: str = Field(..., description="The decision made by the reviewer")
31
+
32
+ def get_related(self) -> list[dict[str, str]]:
33
+ related = super().get_related()
34
+ related.append(
35
+ {
36
+ "prefect.resource.id": self.reviewer_account_id,
37
+ "prefect.resource.role": "infrahub.related.node",
38
+ "infrahub.node.kind": InfrahubKind.GENERICACCOUNT,
39
+ "infrahub.node.id": self.reviewer_account_id,
40
+ "infrahub.reviewer.account.name": self.reviewer_account_name,
41
+ }
42
+ )
43
+ return related
44
+
45
+ def get_resource(self) -> dict[str, str]:
46
+ return {**super().get_resource(), "infrahub.proposed_change.reviewer_decision": self.reviewer_decision}
47
+
48
+
49
+ class ProposedChangeReviewRevokedEvent(ProposedChangeEvent):
50
+ reviewer_account_id: str = Field(..., description="The ID of the user who reviewed the proposed change")
51
+ reviewer_account_name: str = Field(..., description="The name of the user who reviewed the proposed change")
52
+ reviewer_former_decision: str = Field(..., description="The former decision made by the reviewer")
53
+
54
+ def get_related(self) -> list[dict[str, str]]:
55
+ related = super().get_related()
56
+ related.append(
57
+ {
58
+ "prefect.resource.id": self.reviewer_account_id,
59
+ "prefect.resource.role": "infrahub.related.node",
60
+ "infrahub.node.kind": InfrahubKind.GENERICACCOUNT,
61
+ "infrahub.node.id": self.reviewer_account_id,
62
+ "infrahub.reviewer.account.name": self.reviewer_account_name,
63
+ }
64
+ )
65
+ return related
66
+
67
+ def get_resource(self) -> dict[str, str]:
68
+ return {
69
+ **super().get_resource(),
70
+ "infrahub.proposed_change.reviewer_former_decision": self.reviewer_former_decision,
71
+ }
72
+
73
+
74
+ class ProposedChangeMergedEvent(ProposedChangeEvent):
75
+ """Event generated when a proposed change has been merged"""
76
+
77
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.merged"
78
+
79
+ merged_by_account_id: str = Field(..., description="The ID of the user who merged the proposed change")
80
+ merged_by_account_name: str = Field(..., description="The name of the user who merged the proposed change")
81
+
82
+ def get_related(self) -> list[dict[str, str]]:
83
+ related = super().get_related()
84
+ related.append(
85
+ {
86
+ "prefect.resource.id": self.merged_by_account_id,
87
+ "prefect.resource.role": "infrahub.related.node",
88
+ "infrahub.node.kind": InfrahubKind.GENERICACCOUNT,
89
+ "infrahub.node.id": self.merged_by_account_id,
90
+ "infrahub.merged_by.account.name": self.merged_by_account_name,
91
+ }
92
+ )
93
+ return related
94
+
95
+ def get_resource(self) -> dict[str, str]:
96
+ return {
97
+ **super().get_resource(),
98
+ "infrahub.proposed_change.merged_by_account_id": self.merged_by_account_id,
99
+ "infrahub.proposed_change.merged_by_account_name": self.merged_by_account_name,
100
+ }
101
+
102
+
103
+ class ProposedChangeReviewRequestedEvent(ProposedChangeEvent):
104
+ """Event generated when a proposed change has been flagged for review"""
105
+
106
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.review_requested"
107
+
108
+ requested_by_account_id: str = Field(
109
+ ..., description="The ID of the user who requested the proposed change to be reviewed"
110
+ )
111
+ requested_by_account_name: str = Field(
112
+ ..., description="The name of the user who requested the proposed change to be reviewed"
113
+ )
114
+
115
+ def get_related(self) -> list[dict[str, str]]:
116
+ related = super().get_related()
117
+ related.append(
118
+ {
119
+ "prefect.resource.id": self.requested_by_account_id,
120
+ "prefect.resource.role": "infrahub.related.node",
121
+ "infrahub.node.kind": InfrahubKind.GENERICACCOUNT,
122
+ "infrahub.node.id": self.requested_by_account_id,
123
+ "infrahub.requested_by.account.name": self.requested_by_account_name,
124
+ }
125
+ )
126
+ return related
127
+
128
+
129
+ class ProposedChangeApprovedEvent(ProposedChangeReviewEvent):
130
+ """Event generated when a proposed change has been approved"""
131
+
132
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.approved"
133
+
134
+
135
+ class ProposedChangeRejectedEvent(ProposedChangeReviewEvent):
136
+ """Event generated when a proposed change has been rejected"""
137
+
138
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.rejected"
139
+
140
+
141
+ class ProposedChangeApprovalRevokedEvent(ProposedChangeReviewRevokedEvent):
142
+ """Event generated when a proposed change approval has been revoked"""
143
+
144
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.approval_revoked"
145
+
146
+
147
+ class ProposedChangeRejectionRevokedEvent(ProposedChangeReviewRevokedEvent):
148
+ """Event generated when a proposed change rejection has been revoked"""
149
+
150
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.rejection_revoked"
151
+
152
+
153
+ class ProposedChangeThreadEvent(ProposedChangeEvent):
154
+ thread_id: str = Field(..., description="The ID of the thread that was created or updated")
155
+ thread_kind: str = Field(..., description="The name of the thread that was created or updated")
156
+
157
+ def get_related(self) -> list[dict[str, str]]:
158
+ related = super().get_related()
159
+ related.append(
160
+ {
161
+ "prefect.resource.id": self.thread_id,
162
+ "prefect.resource.role": "infrahub.related.node",
163
+ "infrahub.node.kind": self.thread_kind,
164
+ "infrahub.node.id": self.thread_id,
165
+ }
166
+ )
167
+ return related
168
+
169
+
170
+ class ProposedChangeThreadCreatedEvent(ProposedChangeThreadEvent):
171
+ """Event generated when a thread has been created in a proposed change"""
172
+
173
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change_thread.created"
174
+ action: MutationAction = MutationAction.CREATED
175
+
176
+
177
+ class ProposedChangeThreadUpdatedEvent(ProposedChangeThreadEvent):
178
+ """Event generated when a thread has been updated in a proposed change"""
179
+
180
+ event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change_thread.updated"
181
+ action: MutationAction = MutationAction.UPDATED