infrahub-server 1.2.9rc0__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 (166) 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/computed_attribute/models.py +13 -0
  10. infrahub/computed_attribute/tasks.py +48 -26
  11. infrahub/config.py +9 -0
  12. infrahub/core/account.py +24 -47
  13. infrahub/core/attribute.py +53 -14
  14. infrahub/core/branch/models.py +8 -9
  15. infrahub/core/branch/tasks.py +0 -2
  16. infrahub/core/constants/infrahubkind.py +8 -0
  17. infrahub/core/constraint/node/runner.py +1 -1
  18. infrahub/core/convert_object_type/__init__.py +0 -0
  19. infrahub/core/convert_object_type/conversion.py +122 -0
  20. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  21. infrahub/core/diff/calculator.py +65 -11
  22. infrahub/core/diff/combiner.py +38 -31
  23. infrahub/core/diff/coordinator.py +44 -28
  24. infrahub/core/diff/data_check_synchronizer.py +3 -2
  25. infrahub/core/diff/enricher/hierarchy.py +36 -27
  26. infrahub/core/diff/ipam_diff_parser.py +5 -4
  27. infrahub/core/diff/merger/merger.py +46 -16
  28. infrahub/core/diff/merger/serializer.py +1 -0
  29. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  30. infrahub/core/diff/model/path.py +58 -58
  31. infrahub/core/diff/parent_node_adder.py +14 -16
  32. infrahub/core/diff/query/all_conflicts.py +1 -5
  33. infrahub/core/diff/query/artifact.py +10 -20
  34. infrahub/core/diff/query/diff_get.py +3 -6
  35. infrahub/core/diff/query/drop_nodes.py +42 -0
  36. infrahub/core/diff/query/field_specifiers.py +8 -7
  37. infrahub/core/diff/query/field_summary.py +2 -4
  38. infrahub/core/diff/query/filters.py +15 -1
  39. infrahub/core/diff/query/merge.py +284 -101
  40. infrahub/core/diff/query/save.py +26 -34
  41. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  42. infrahub/core/diff/query_parser.py +55 -65
  43. infrahub/core/diff/repository/deserializer.py +38 -24
  44. infrahub/core/diff/repository/repository.py +31 -12
  45. infrahub/core/diff/tasks.py +3 -3
  46. infrahub/core/graph/__init__.py +1 -1
  47. infrahub/core/manager.py +14 -11
  48. infrahub/core/migrations/graph/__init__.py +2 -0
  49. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  50. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  51. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  52. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  53. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  54. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  55. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  56. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  57. infrahub/core/migrations/query/attribute_add.py +1 -2
  58. infrahub/core/migrations/query/attribute_rename.py +3 -6
  59. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  60. infrahub/core/migrations/query/node_duplicate.py +3 -6
  61. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  62. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  63. infrahub/core/migrations/schema/node_remove.py +3 -6
  64. infrahub/core/models.py +29 -2
  65. infrahub/core/node/__init__.py +18 -4
  66. infrahub/core/node/create.py +211 -0
  67. infrahub/core/protocols.py +51 -0
  68. infrahub/core/protocols_base.py +3 -0
  69. infrahub/core/query/__init__.py +2 -2
  70. infrahub/core/query/branch.py +27 -17
  71. infrahub/core/query/diff.py +186 -81
  72. infrahub/core/query/ipam.py +10 -20
  73. infrahub/core/query/node.py +65 -49
  74. infrahub/core/query/relationship.py +156 -58
  75. infrahub/core/query/resource_manager.py +1 -2
  76. infrahub/core/query/subquery.py +4 -6
  77. infrahub/core/relationship/model.py +4 -1
  78. infrahub/core/schema/__init__.py +2 -1
  79. infrahub/core/schema/attribute_parameters.py +36 -0
  80. infrahub/core/schema/attribute_schema.py +83 -8
  81. infrahub/core/schema/basenode_schema.py +25 -1
  82. infrahub/core/schema/definitions/core/__init__.py +21 -0
  83. infrahub/core/schema/definitions/internal.py +13 -3
  84. infrahub/core/schema/generated/attribute_schema.py +9 -3
  85. infrahub/core/schema/schema_branch.py +15 -7
  86. infrahub/core/validators/__init__.py +5 -1
  87. infrahub/core/validators/attribute/choices.py +1 -2
  88. infrahub/core/validators/attribute/enum.py +1 -2
  89. infrahub/core/validators/attribute/kind.py +1 -2
  90. infrahub/core/validators/attribute/length.py +13 -6
  91. infrahub/core/validators/attribute/optional.py +1 -2
  92. infrahub/core/validators/attribute/regex.py +5 -5
  93. infrahub/core/validators/attribute/unique.py +1 -3
  94. infrahub/core/validators/determiner.py +18 -2
  95. infrahub/core/validators/enum.py +7 -0
  96. infrahub/core/validators/node/hierarchy.py +3 -6
  97. infrahub/core/validators/query.py +1 -3
  98. infrahub/core/validators/relationship/count.py +6 -12
  99. infrahub/core/validators/relationship/optional.py +2 -4
  100. infrahub/core/validators/relationship/peer.py +3 -8
  101. infrahub/core/validators/tasks.py +1 -1
  102. infrahub/core/validators/uniqueness/query.py +12 -9
  103. infrahub/database/__init__.py +1 -3
  104. infrahub/events/group_action.py +1 -0
  105. infrahub/graphql/analyzer.py +139 -18
  106. infrahub/graphql/app.py +1 -1
  107. infrahub/graphql/loaders/node.py +1 -1
  108. infrahub/graphql/loaders/peers.py +1 -1
  109. infrahub/graphql/manager.py +4 -0
  110. infrahub/graphql/mutations/action.py +164 -0
  111. infrahub/graphql/mutations/convert_object_type.py +62 -0
  112. infrahub/graphql/mutations/main.py +24 -175
  113. infrahub/graphql/mutations/proposed_change.py +21 -18
  114. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  115. infrahub/graphql/queries/diff/tree.py +2 -1
  116. infrahub/graphql/queries/relationship.py +1 -1
  117. infrahub/graphql/resolvers/many_relationship.py +4 -4
  118. infrahub/graphql/resolvers/resolver.py +4 -4
  119. infrahub/graphql/resolvers/single_relationship.py +2 -2
  120. infrahub/graphql/schema.py +6 -0
  121. infrahub/graphql/subscription/graphql_query.py +2 -2
  122. infrahub/graphql/types/branch.py +1 -1
  123. infrahub/menu/menu.py +31 -0
  124. infrahub/message_bus/messages/__init__.py +0 -10
  125. infrahub/message_bus/operations/__init__.py +0 -8
  126. infrahub/message_bus/operations/refresh/registry.py +1 -1
  127. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  128. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  129. infrahub/prefect_server/models.py +1 -19
  130. infrahub/proposed_change/models.py +68 -3
  131. infrahub/proposed_change/tasks.py +907 -30
  132. infrahub/task_manager/models.py +10 -6
  133. infrahub/telemetry/database.py +1 -1
  134. infrahub/telemetry/tasks.py +1 -1
  135. infrahub/trigger/catalogue.py +2 -0
  136. infrahub/trigger/models.py +29 -3
  137. infrahub/trigger/setup.py +51 -15
  138. infrahub/trigger/tasks.py +4 -5
  139. infrahub/types.py +1 -1
  140. infrahub/webhook/models.py +2 -1
  141. infrahub/workflows/catalogue.py +85 -0
  142. infrahub/workflows/initialization.py +1 -3
  143. infrahub_sdk/timestamp.py +2 -2
  144. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +4 -4
  145. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +153 -146
  146. infrahub_testcontainers/container.py +0 -1
  147. infrahub_testcontainers/docker-compose.test.yml +4 -4
  148. infrahub_testcontainers/helpers.py +8 -2
  149. infrahub_testcontainers/performance_test.py +6 -3
  150. infrahub/message_bus/messages/check_generator_run.py +0 -26
  151. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  152. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  153. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  154. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  155. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  156. infrahub/message_bus/operations/check/__init__.py +0 -3
  157. infrahub/message_bus/operations/check/generator.py +0 -156
  158. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  159. infrahub/message_bus/operations/finalize/validator.py +0 -133
  160. infrahub/message_bus/operations/requests/__init__.py +0 -9
  161. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  162. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  163. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  164. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  165. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  166. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -73,9 +73,18 @@ class RelationshipPeerData:
73
73
  source_id: UUID
74
74
  """UUID of the Source Node."""
75
75
 
76
+ source_db_id: str
77
+ """Internal DB ID of the Source Node."""
78
+
79
+ source_kind: str
80
+ """Kind of the Source Node."""
81
+
76
82
  peer_id: UUID
77
83
  """UUID of the Peer Node."""
78
84
 
85
+ peer_db_id: str
86
+ """Internal DB ID of the Peer Node."""
87
+
79
88
  peer_kind: str
80
89
  """Kind of the Peer Node."""
81
90
 
@@ -85,9 +94,6 @@ class RelationshipPeerData:
85
94
  rel_node_id: UUID | None = None
86
95
  """UUID of the Relationship Node."""
87
96
 
88
- peer_db_id: str | None = None
89
- """Internal DB ID of the Peer Node."""
90
-
91
97
  rel_node_db_id: str | None = None
92
98
  """Internal DB ID of the Relationship Node."""
93
99
 
@@ -196,6 +202,63 @@ class RelationshipQuery(Query):
196
202
  rel_prop_dict["hierarchy"] = self.schema.hierarchical
197
203
  return rel_prop_dict
198
204
 
205
+ def add_source_match_to_query(self, source_branch: Branch) -> None:
206
+ self.params["source_id"] = self.source_id or self.source.get_id()
207
+ if source_branch.is_global or source_branch.is_default:
208
+ source_query_match = """
209
+ MATCH (s:Node { uuid: $source_id })
210
+ OPTIONAL MATCH (s)-[delete_edge:IS_PART_OF {status: "deleted", branch: $source_branch}]->(:Root)
211
+ WHERE delete_edge.from <= $at
212
+ WITH *, s WHERE delete_edge IS NULL
213
+ """
214
+ self.params["source_branch"] = source_branch.name
215
+ source_filter, source_filter_params = source_branch.get_query_filter_path(
216
+ at=self.at, variable_name="r", params_prefix="src_"
217
+ )
218
+ source_query_match = """
219
+ MATCH (s:Node { uuid: $source_id })
220
+ CALL {
221
+ WITH s
222
+ MATCH (s)-[r:IS_PART_OF]->(:Root)
223
+ WHERE %(source_filter)s
224
+ RETURN r.status = "active" AS s_is_active
225
+ ORDER BY r.from DESC
226
+ LIMIT 1
227
+ }
228
+ WITH *, s WHERE s_is_active = TRUE
229
+ """ % {"source_filter": source_filter}
230
+ self.params.update(source_filter_params)
231
+ self.add_to_query(source_query_match)
232
+
233
+ def add_dest_match_to_query(self, destination_branch: Branch, destination_id: str) -> None:
234
+ self.params["destination_id"] = destination_id
235
+ if destination_branch.is_global or destination_branch.is_default:
236
+ destination_query_match = """
237
+ MATCH (d:Node { uuid: $destination_id })
238
+ OPTIONAL MATCH (d)-[delete_edge:IS_PART_OF {status: "deleted", branch: $destination_branch}]->(:Root)
239
+ WHERE delete_edge.from <= $at
240
+ WITH *, d WHERE delete_edge IS NULL
241
+ """
242
+ self.params["destination_branch"] = destination_branch.name
243
+ else:
244
+ destination_filter, destination_filter_params = destination_branch.get_query_filter_path(
245
+ at=self.at, variable_name="r", params_prefix="dst_"
246
+ )
247
+ destination_query_match = """
248
+ MATCH (d:Node { uuid: $destination_id })
249
+ CALL {
250
+ WITH d
251
+ MATCH (d)-[r:IS_PART_OF]->(:Root)
252
+ WHERE %(destination_filter)s
253
+ RETURN r.status = "active" AS d_is_active
254
+ ORDER BY r.from DESC
255
+ LIMIT 1
256
+ }
257
+ WITH *, d WHERE d_is_active = TRUE
258
+ """ % {"destination_filter": destination_filter}
259
+ self.params.update(destination_filter_params)
260
+ self.add_to_query(destination_query_match)
261
+
199
262
 
200
263
  class RelationshipCreateQuery(RelationshipQuery):
201
264
  name = "relationship_create"
@@ -214,8 +277,6 @@ class RelationshipCreateQuery(RelationshipQuery):
214
277
  super().__init__(destination=destination, destination_id=destination_id, **kwargs)
215
278
 
216
279
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
217
- self.params["source_id"] = self.source_id
218
- self.params["destination_id"] = self.destination_id
219
280
  self.params["name"] = self.schema.identifier
220
281
  self.params["branch_support"] = self.schema.branch.value
221
282
 
@@ -228,12 +289,11 @@ class RelationshipCreateQuery(RelationshipQuery):
228
289
  self.params["is_protected"] = self.rel.is_protected
229
290
  self.params["is_visible"] = self.rel.is_visible
230
291
 
231
- query_match = """
232
- MATCH (s:Node { uuid: $source_id })
233
- MATCH (d:Node { uuid: $destination_id })
234
- """
235
- self.add_to_query(query_match)
236
-
292
+ self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
293
+ self.add_dest_match_to_query(
294
+ destination_branch=self.destination.get_branch_based_on_support_type(),
295
+ destination_id=self.destination_id or self.destination.get_id(),
296
+ )
237
297
  self.query_add_all_node_property_match()
238
298
 
239
299
  self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.ACTIVE)
@@ -378,7 +438,6 @@ class RelationshipDataDeleteQuery(RelationshipQuery):
378
438
 
379
439
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
380
440
  self.params["source_id"] = self.source_id
381
- self.params["destination_id"] = self.data.peer_id
382
441
  self.params["rel_node_id"] = self.data.rel_node_id
383
442
  self.params["name"] = self.schema.identifier
384
443
  self.params["branch"] = self.branch.name
@@ -388,9 +447,10 @@ class RelationshipDataDeleteQuery(RelationshipQuery):
388
447
  # -----------------------------------------------------------------------
389
448
  # Match all nodes, including properties
390
449
  # -----------------------------------------------------------------------
450
+
451
+ self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
452
+ self.add_dest_match_to_query(destination_branch=self.branch, destination_id=self.data.peer_id)
391
453
  query = """
392
- MATCH (s:Node { uuid: $source_id })
393
- MATCH (d:Node { uuid: $destination_id })
394
454
  MATCH (rl:Relationship { uuid: $rel_node_id })
395
455
  """
396
456
  self.add_to_query(query)
@@ -442,8 +502,6 @@ class RelationshipDeleteQuery(RelationshipQuery):
442
502
 
443
503
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
444
504
  rel_filter, rel_params = self.branch.get_query_filter_path(at=self.at, variable_name="edge")
445
- self.params["source_id"] = self.source_id
446
- self.params["destination_id"] = self.destination_id
447
505
  self.params["rel_id"] = self.rel.id
448
506
  self.params["branch"] = self.branch.name
449
507
  self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.DELETED)
@@ -454,15 +512,19 @@ class RelationshipDeleteQuery(RelationshipQuery):
454
512
  r1 = f"{arrows.left.start}[r1:{self.rel_type} $rel_prop ]{arrows.left.end}"
455
513
  r2 = f"{arrows.right.start}[r2:{self.rel_type} $rel_prop ]{arrows.right.end}"
456
514
 
515
+ self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
516
+ self.add_dest_match_to_query(
517
+ destination_branch=self.destination.get_branch_based_on_support_type(),
518
+ destination_id=self.destination_id or self.destination.get_id(),
519
+ )
457
520
  query = """
458
- MATCH (s:Node { uuid: $source_id })-[:IS_RELATED]-(rl:Relationship {uuid: $rel_id})-[:IS_RELATED]-(d:Node { uuid: $destination_id })
459
- WITH s, rl, d
521
+ MATCH (s)-[:IS_RELATED]-(rl:Relationship {uuid: $rel_id})-[:IS_RELATED]-(d)
522
+ WITH DISTINCT s, rl, d
460
523
  LIMIT 1
461
524
  CREATE (s)%(r1)s(rl)
462
525
  CREATE (rl)%(r2)s(d)
463
526
  WITH rl
464
- CALL {
465
- WITH rl
527
+ CALL (rl) {
466
528
  MATCH (rl)-[edge:IS_VISIBLE]->(visible)
467
529
  WHERE %(rel_filter)s AND edge.status = "active"
468
530
  WITH rl, edge, visible
@@ -473,8 +535,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
473
535
  WHERE edge.branch = $branch
474
536
  SET edge.to = $at
475
537
  }
476
- CALL {
477
- WITH rl
538
+ CALL (rl) {
478
539
  MATCH (rl)-[edge:IS_PROTECTED]->(protected)
479
540
  WHERE %(rel_filter)s AND edge.status = "active"
480
541
  WITH rl, edge, protected
@@ -485,8 +546,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
485
546
  WHERE edge.branch = $branch
486
547
  SET edge.to = $at
487
548
  }
488
- CALL {
489
- WITH rl
549
+ CALL (rl) {
490
550
  MATCH (rl)-[edge:HAS_OWNER]->(owner_node)
491
551
  WHERE %(rel_filter)s AND edge.status = "active"
492
552
  WITH rl, edge, owner_node
@@ -497,8 +557,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
497
557
  WHERE edge.branch = $branch
498
558
  SET edge.to = $at
499
559
  }
500
- CALL {
501
- WITH rl
560
+ CALL (rl) {
502
561
  MATCH (rl)-[edge:HAS_SOURCE]->(source_node)
503
562
  WHERE %(rel_filter)s AND edge.status = "active"
504
563
  WITH rl, edge, source_node
@@ -593,8 +652,7 @@ class RelationshipGetPeerQuery(Query):
593
652
  MATCH (source_node:Node)%(arrow_left_start)s[:IS_RELATED]%(arrow_left_end)s(rl:Relationship { name: $rel_identifier })
594
653
  WHERE source_node.uuid IN $source_ids
595
654
  WITH DISTINCT source_node, rl
596
- CALL {
597
- WITH rl, source_node
655
+ CALL (rl, source_node) {
598
656
  MATCH path = (source_node)%(path)s(peer:Node)
599
657
  WHERE
600
658
  $source_kind IN LABELS(source_node) AND
@@ -663,22 +721,19 @@ class RelationshipGetPeerQuery(Query):
663
721
  with_str = ", ".join(
664
722
  [f"{subquery_result_name} as {label}" if label == "peer" else label for label in self.return_labels]
665
723
  )
666
- self.add_subquery(subquery=subquery, with_clause=with_str)
667
-
724
+ self.add_subquery(subquery=subquery, node_alias="peer", with_clause=with_str)
668
725
  # ----------------------------------------------------------------------------
669
726
  # QUERY Properties
670
727
  # ----------------------------------------------------------------------------
671
728
  query = """
672
- CALL {
673
- WITH rl
729
+ CALL (rl) {
674
730
  MATCH (rl)-[r:IS_VISIBLE]-(is_visible)
675
731
  WHERE %(branch_filter)s
676
732
  RETURN r AS rel_is_visible, is_visible
677
733
  ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
678
734
  LIMIT 1
679
735
  }
680
- CALL {
681
- WITH rl
736
+ CALL (rl) {
682
737
  MATCH (rl)-[r:IS_PROTECTED]-(is_protected)
683
738
  WHERE %(branch_filter)s
684
739
  RETURN r AS rel_is_protected, is_protected
@@ -695,8 +750,7 @@ class RelationshipGetPeerQuery(Query):
695
750
  # We must query them one by one otherwise the second one won't return
696
751
  for node_prop in ["source", "owner"]:
697
752
  query = """
698
- CALL {
699
- WITH rl
753
+ CALL (rl) {
700
754
  OPTIONAL MATCH (rl)-[r:HAS_%(node_prop_type)s]-(%(node_prop)s)
701
755
  WHERE %(branch_filter)s
702
756
  RETURN r AS rel_%(node_prop)s, %(node_prop)s
@@ -737,7 +791,7 @@ class RelationshipGetPeerQuery(Query):
737
791
  self.order_by.append(subquery_result_name)
738
792
  self.params.update(subquery_params)
739
793
 
740
- self.add_subquery(subquery=subquery)
794
+ self.add_subquery(subquery=subquery, node_alias="peer")
741
795
 
742
796
  order_cnt += 1
743
797
 
@@ -752,10 +806,15 @@ class RelationshipGetPeerQuery(Query):
752
806
  def get_peers(self) -> Generator[RelationshipPeerData, None, None]:
753
807
  for result in self.get_results_group_by(("peer", "uuid"), ("source_node", "uuid")):
754
808
  rels = result.get("rels")
809
+ source_node = result.get_node("source_node")
810
+ peer_node = result.get_node("peer")
755
811
  data = RelationshipPeerData(
756
- source_id=result.get_node("source_node").get("uuid"),
757
- peer_id=result.get_node("peer").get("uuid"),
758
- peer_kind=result.get_node("peer").get("kind"),
812
+ source_id=source_node.get("uuid"),
813
+ source_db_id=source_node.element_id,
814
+ source_kind=source_node.get("kind"),
815
+ peer_id=peer_node.get("uuid"),
816
+ peer_db_id=peer_node.element_id,
817
+ peer_kind=peer_node.get("kind"),
759
818
  rel_node_db_id=result.get("rl").element_id,
760
819
  rel_node_id=result.get("rl").get("uuid"),
761
820
  updated_at=rels[0]["from"],
@@ -793,8 +852,6 @@ class RelationshipGetQuery(RelationshipQuery):
793
852
  type: QueryType = QueryType.READ
794
853
 
795
854
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
796
- self.params["source_id"] = self.source_id
797
- self.params["destination_id"] = self.destination_id
798
855
  self.params["name"] = self.schema.identifier
799
856
  self.params["branch"] = self.branch.name
800
857
 
@@ -808,9 +865,12 @@ class RelationshipGetQuery(RelationshipQuery):
808
865
  r1 = f"{arrows.left.start}[r1:{self.rel.rel_type}]{arrows.left.end}"
809
866
  r2 = f"{arrows.right.start}[r2:{self.rel.rel_type}]{arrows.right.end}"
810
867
 
868
+ self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
869
+ self.add_dest_match_to_query(
870
+ destination_branch=self.destination.get_branch_based_on_support_type(),
871
+ destination_id=self.destination_id or self.destination.get_id(),
872
+ )
811
873
  query = """
812
- MATCH (s:Node { uuid: $source_id })
813
- MATCH (d:Node { uuid: $destination_id })
814
874
  MATCH (s)%s(rl:Relationship { name: $name })%s(d)
815
875
  WHERE %s
816
876
  """ % (
@@ -870,8 +930,7 @@ class RelationshipGetByIdentifierQuery(Query):
870
930
  query = """
871
931
  MATCH (rl:Relationship)
872
932
  WHERE rl.name IN $identifiers
873
- CALL {
874
- WITH rl
933
+ CALL (rl) {
875
934
  MATCH (src:Node)-[r1:IS_RELATED]-(rl:Relationship)-[r2:IS_RELATED]-(dst:Node)
876
935
  WHERE (size($full_identifiers) = 0 OR [src.kind, rl.name, dst.kind] in $full_identifiers)
877
936
  AND NOT src.namespace IN $excluded_namespaces
@@ -934,8 +993,7 @@ class RelationshipCountPerNodeQuery(Query):
934
993
  query = """
935
994
  MATCH (peer_node:Node)%(path)s(rl:Relationship { name: $rel_identifier })
936
995
  WHERE peer_node.uuid IN $peer_ids AND %(branch_filter)s
937
- CALL {
938
- WITH rl
996
+ CALL (rl) {
939
997
  MATCH path = (peer_node:Node)%(path)s(rl)
940
998
  WHERE peer_node.uuid IN $peer_ids AND %(branch_filter)s
941
999
  RETURN peer_node as peer, r as r1
@@ -1013,8 +1071,7 @@ class RelationshipDeleteAllQuery(Query):
1013
1071
  for arrow_left, arrow_right in (("<-", "-"), ("-", "->")):
1014
1072
  for edge_type in edge_types:
1015
1073
  sub_query = """
1016
- CALL {
1017
- WITH rl
1074
+ CALL (rl) {
1018
1075
  MATCH (rl)%(arrow_left)s[active_edge:%(edge_type)s]%(arrow_right)s(n)
1019
1076
  WHERE %(active_rel_filter)s AND active_edge.status ="active"
1020
1077
  CREATE (rl)%(arrow_left)s[deleted_edge:%(edge_type)s $rel_prop]%(arrow_right)s(n)
@@ -1034,10 +1091,13 @@ class RelationshipDeleteAllQuery(Query):
1034
1091
 
1035
1092
  # We only want to return uuid/kind of `Node` connected through `IS_RELATED` edges.
1036
1093
  query += """
1037
- CALL {
1038
- WITH rl
1094
+ CALL (rl) {
1039
1095
  MATCH (rl)-[active_edge:IS_RELATED]->(n)
1040
- WHERE %(active_rel_filter)s AND active_edge.status ="active"
1096
+ WHERE %(active_rel_filter)s
1097
+ WITH rl, active_edge, n
1098
+ ORDER BY %(id_func)s(rl), %(id_func)s(n), active_edge.from DESC
1099
+ WITH rl, n, head(collect(active_edge)) AS active_edge
1100
+ WHERE active_edge.status = "active"
1041
1101
  CREATE (rl)-[deleted_edge:IS_RELATED $rel_prop]->(n)
1042
1102
  SET deleted_edge.hierarchy = active_edge.hierarchy
1043
1103
  WITH rl, active_edge, n
@@ -1050,10 +1110,13 @@ class RelationshipDeleteAllQuery(Query):
1050
1110
  "outbound" as rel_direction
1051
1111
 
1052
1112
  UNION
1053
-
1054
1113
  WITH rl
1055
1114
  MATCH (rl)<-[active_edge:IS_RELATED]-(n)
1056
- WHERE %(active_rel_filter)s AND active_edge.status ="active"
1115
+ WHERE %(active_rel_filter)s
1116
+ WITH rl, active_edge, n
1117
+ ORDER BY %(id_func)s(rl), %(id_func)s(n), active_edge.from DESC
1118
+ WITH rl, n, head(collect(active_edge)) AS active_edge
1119
+ WHERE active_edge.status = "active"
1057
1120
  CREATE (rl)<-[deleted_edge:IS_RELATED $rel_prop]-(n)
1058
1121
  SET deleted_edge.hierarchy = active_edge.hierarchy
1059
1122
  WITH rl, active_edge, n
@@ -1066,9 +1129,7 @@ class RelationshipDeleteAllQuery(Query):
1066
1129
  "inbound" as rel_direction
1067
1130
  }
1068
1131
  RETURN DISTINCT uuid, kind, rel_identifier, rel_direction
1069
- """ % {
1070
- "active_rel_filter": active_rel_filter,
1071
- }
1132
+ """ % {"active_rel_filter": active_rel_filter, "id_func": db.get_id_function_name()}
1072
1133
 
1073
1134
  self.add_to_query(query)
1074
1135
 
@@ -1118,3 +1179,40 @@ class RelationshipDeleteAllQuery(Query):
1118
1179
  changelog_mapper.delete_relationship(peer_id=peer_uuid, peer_kind=kind, rel_schema=deleted_rel_schema)
1119
1180
 
1120
1181
  return [changelog_mapper.changelog for changelog_mapper in rel_identifier_to_changelog_mapper.values()]
1182
+
1183
+
1184
+ class GetAllPeersIds(Query):
1185
+ """
1186
+ Return all peers ids connected to input node. Some peers can be excluded using `exclude_identifiers`.
1187
+ """
1188
+
1189
+ name = "get_peers_ids"
1190
+ type: QueryType = QueryType.READ
1191
+ insert_return = False
1192
+
1193
+ def __init__(self, node_id: str, exclude_identifiers: list[str], **kwargs):
1194
+ self.node_id = node_id
1195
+ self.exclude_identifiers = exclude_identifiers
1196
+ super().__init__(**kwargs)
1197
+
1198
+ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
1199
+ self.params["source_id"] = kwargs["node_id"]
1200
+ self.params["branch"] = self.branch.name
1201
+ self.params["exclude_identifiers"] = self.exclude_identifiers
1202
+
1203
+ active_rel_filter, rel_params = self.branch.get_query_filter_path(
1204
+ at=self.at, variable_name="e1", branch_agnostic=self.branch_agnostic
1205
+ )
1206
+ self.params.update(rel_params)
1207
+
1208
+ query = """
1209
+ MATCH (node:Node { uuid: $source_id })-[e1:IS_RELATED]-(rl:Relationship)-[e2:IS_RELATED]-(peer:Node)
1210
+ WHERE %(active_rel_filter)s AND peer.uuid <> node.uuid AND NOT (rl.name IN $exclude_identifiers)
1211
+ WITH DISTINCT(peer.uuid) as uuid
1212
+ RETURN uuid
1213
+ """ % {"active_rel_filter": active_rel_filter}
1214
+
1215
+ self.add_to_query(query)
1216
+
1217
+ def get_peers_uuids(self) -> list[str]:
1218
+ return [row.data["uuid"] for row in self.results] # type: ignore
@@ -220,8 +220,7 @@ class NumberPoolGetUsed(Query):
220
220
 
221
221
  query = """
222
222
  MATCH (pool:%(number_pool)s { uuid: $pool_id })
223
- CALL {
224
- WITH pool
223
+ CALL (pool) {
225
224
  MATCH (pool)-[res:IS_RESERVED]->(av:AttributeValue)<-[hv:HAS_VALUE]-(attr:Attribute)
226
225
  WHERE
227
226
  attr.name = $attribute_name
@@ -57,11 +57,11 @@ async def build_subquery_filter(
57
57
  params.update(field_params)
58
58
 
59
59
  field_where.append("all(r IN relationships(path) WHERE (%s))" % branch_filter)
60
- filter_str = f"({node_alias})" + "".join([str(item) for item in field_filter])
60
+ filter_str = f"({node_alias}:Node {{uuid: {node_alias}.uuid}})" + "".join([str(item) for item in field_filter])
61
61
  where_str = " AND ".join(field_where)
62
62
  branch_level_str = "reduce(br_lvl = 0, r in relationships(path) | br_lvl + r.branch_level)"
63
63
  froms_str = db.render_list_comprehension(items="relationships(path)", item_name="from")
64
- to_return = f"{node_alias} AS {prefix}"
64
+ to_return = f"{prefix}"
65
65
  with_extra = ""
66
66
  final_with_extra = ""
67
67
  is_isnull = filter_name == "isnull"
@@ -82,7 +82,6 @@ async def build_subquery_filter(
82
82
  elif field is not None and field.is_attribute:
83
83
  is_active_filter = "(latest_node_details[2]).value = 'NULL'"
84
84
  query = f"""
85
- WITH {node_alias}
86
85
  {match} path = {filter_str}
87
86
  WHERE {where_str}
88
87
  WITH
@@ -94,7 +93,7 @@ async def build_subquery_filter(
94
93
  ORDER BY branch_level DESC, froms[-1] DESC, froms[-2] DESC
95
94
  WITH head(collect([is_active, {node_alias}{with_extra}])) AS latest_node_details
96
95
  WHERE {is_active_filter}
97
- WITH latest_node_details[1] AS {node_alias}{final_with_extra}
96
+ WITH latest_node_details[1] AS {prefix}{final_with_extra}
98
97
  RETURN {to_return}
99
98
  """
100
99
  return query, params, prefix
@@ -138,7 +137,7 @@ async def build_subquery_order(
138
137
  field_filter[-1].name = "last"
139
138
 
140
139
  field_where.append("all(r IN relationships(path) WHERE (%s))" % branch_filter)
141
- filter_str = f"({node_alias})" + "".join([str(item) for item in field_filter])
140
+ filter_str = f"({node_alias}:Node {{uuid: {node_alias}.uuid}})" + "".join([str(item) for item in field_filter])
142
141
  where_str = " AND ".join(field_where)
143
142
  branch_level_str = "reduce(br_lvl = 0, r in relationships(path) | br_lvl + r.branch_level)"
144
143
  froms_str = db.render_list_comprehension(items="relationships(path)", item_name="from")
@@ -174,7 +173,6 @@ async def build_subquery_order(
174
173
  to_return_str_parts.append(f"CASE WHEN is_active = TRUE THEN {expression} ELSE NULL END AS {alias}")
175
174
  to_return_str = ", ".join(to_return_str_parts)
176
175
  query = f"""
177
- WITH {node_alias}
178
176
  OPTIONAL MATCH path = {filter_str}
179
177
  WHERE {where_str}
180
178
  WITH {with_str_to_alias}
@@ -416,7 +416,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
416
416
  await update_relationships_to(rel_ids_to_update, to=delete_at, db=db)
417
417
 
418
418
  delete_query = await RelationshipDeleteQuery.init(
419
- db=db, rel=self, source_id=node.id, destination_id=peer.id, branch=branch, at=delete_at
419
+ db=db, rel=self, source=node, destination=peer, branch=branch, at=delete_at
420
420
  )
421
421
  await delete_query.execute(db=db)
422
422
 
@@ -789,6 +789,9 @@ class RelationshipManager:
789
789
 
790
790
  return len(self._relationships)
791
791
 
792
+ def validate(self) -> None:
793
+ self._relationships.validate()
794
+
792
795
  @overload
793
796
  async def get_peer(
794
797
  self,
@@ -21,7 +21,8 @@ from .profile_schema import ProfileSchema
21
21
  from .relationship_schema import RelationshipSchema
22
22
  from .template_schema import TemplateSchema
23
23
 
24
- MainSchemaTypes: TypeAlias = NodeSchema | GenericSchema | ProfileSchema | TemplateSchema
24
+ NonGenericSchemaTypes: TypeAlias = NodeSchema | ProfileSchema | TemplateSchema
25
+ MainSchemaTypes: TypeAlias = NonGenericSchemaTypes | GenericSchema
25
26
 
26
27
 
27
28
  # -----------------------------------------------------
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+
5
+ from infrahub.core.constants.schema import UpdateSupport
6
+ from infrahub.core.models import HashableModel
7
+
8
+
9
+ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParameters]:
10
+ return {
11
+ "Text": TextAttributeParameters,
12
+ "TextArea": TextAttributeParameters,
13
+ }.get(kind, AttributeParameters)
14
+
15
+
16
+ class AttributeParameters(HashableModel):
17
+ class Config:
18
+ extra = "forbid"
19
+
20
+
21
+ class TextAttributeParameters(AttributeParameters):
22
+ regex: str | None = Field(
23
+ default=None,
24
+ description="Regular expression that attribute value must match if defined",
25
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
26
+ )
27
+ min_length: int | None = Field(
28
+ default=None,
29
+ description="Set a minimum number of characters allowed.",
30
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
31
+ )
32
+ max_length: int | None = Field(
33
+ default=None,
34
+ description="Set a maximum number of characters allowed.",
35
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
36
+ )
@@ -2,15 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  from enum import Enum
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, Self
6
6
 
7
- from pydantic import field_validator, model_validator
7
+ from pydantic import Field, ValidationInfo, field_validator, model_validator
8
8
 
9
9
  from infrahub import config
10
+ from infrahub.core.constants.schema import UpdateSupport
10
11
  from infrahub.core.enums import generate_python_enum
11
12
  from infrahub.core.query.attribute import default_attribute_query_filter
12
13
  from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
13
14
 
15
+ from .attribute_parameters import AttributeParameters, TextAttributeParameters, get_attribute_parameters_class_for_kind
14
16
  from .generated.attribute_schema import GeneratedAttributeSchema
15
17
 
16
18
  if TYPE_CHECKING:
@@ -21,6 +23,14 @@ if TYPE_CHECKING:
21
23
  from infrahub.database import InfrahubDatabase
22
24
 
23
25
 
26
+ def get_attribute_schema_class_for_kind(kind: str) -> type[AttributeSchema]:
27
+ attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
28
+ "Text": TextAttributeSchema,
29
+ "TextArea": TextAttributeSchema,
30
+ }
31
+ return attribute_schema_class_by_kind.get(kind, AttributeSchema)
32
+
33
+
24
34
  class AttributeSchema(GeneratedAttributeSchema):
25
35
  _sort_by: list[str] = ["name"]
26
36
  _enum_class: type[enum.Enum] | None = None
@@ -53,16 +63,36 @@ class AttributeSchema(GeneratedAttributeSchema):
53
63
 
54
64
  @model_validator(mode="before")
55
65
  @classmethod
56
- def validate_dropdown_choices(cls, values: dict[str, Any]) -> dict[str, Any]:
66
+ def validate_dropdown_choices(cls, values: Any) -> Any:
57
67
  """Validate that choices are defined for a dropdown but not for other kinds."""
58
- if values.get("kind") != "Dropdown" and values.get("choices"):
59
- raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {values['kind']}")
60
-
61
- if values.get("kind") == "Dropdown" and not values.get("choices"):
68
+ if isinstance(values, dict):
69
+ kind = values.get("kind")
70
+ choices = values.get("choices")
71
+ elif isinstance(values, AttributeSchema):
72
+ kind = values.kind
73
+ choices = values.choices
74
+ else:
75
+ return values
76
+ if kind != "Dropdown" and choices:
77
+ raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {kind}")
78
+
79
+ if kind == "Dropdown" and not choices:
62
80
  raise ValueError("The property 'choices' is required for kind=Dropdown")
63
81
 
64
82
  return values
65
83
 
84
+ @field_validator("parameters", mode="before")
85
+ @classmethod
86
+ def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
87
+ """Override parameters class if using base AttributeParameters class and should be using a subclass"""
88
+ kind = info.data["kind"]
89
+ expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
90
+ if value is None:
91
+ return expected_parameters_class()
92
+ if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
93
+ return expected_parameters_class(**value.model_dump())
94
+ return value
95
+
66
96
  def get_class(self) -> type[BaseAttribute]:
67
97
  return ATTRIBUTE_TYPES[self.kind].get_infrahub_class()
68
98
 
@@ -106,7 +136,7 @@ class AttributeSchema(GeneratedAttributeSchema):
106
136
 
107
137
  def to_node(self) -> dict[str, Any]:
108
138
  fields_to_exclude = {"id", "state", "filters"}
109
- fields_to_json = {"computed_attribute"}
139
+ fields_to_json = {"computed_attribute", "parameters"}
110
140
  data = self.model_dump(exclude=fields_to_exclude | fields_to_json)
111
141
 
112
142
  for field_name in fields_to_json:
@@ -117,6 +147,15 @@ class AttributeSchema(GeneratedAttributeSchema):
117
147
 
118
148
  return data
119
149
 
150
+ def get_regex(self) -> str | None:
151
+ return self.regex
152
+
153
+ def get_min_length(self) -> int | None:
154
+ return self.min_length
155
+
156
+ def get_max_length(self) -> int | None:
157
+ return self.max_length
158
+
120
159
  async def get_query_filter(
121
160
  self,
122
161
  name: str,
@@ -144,3 +183,39 @@ class AttributeSchema(GeneratedAttributeSchema):
144
183
  partial_match=partial_match,
145
184
  support_profiles=support_profiles,
146
185
  )
186
+
187
+
188
+ class TextAttributeSchema(AttributeSchema):
189
+ parameters: TextAttributeParameters = Field(
190
+ default_factory=TextAttributeParameters,
191
+ description="Extra parameters specific to text attributes",
192
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
193
+ )
194
+
195
+ @model_validator(mode="after")
196
+ def reconcile_parameters(self) -> Self:
197
+ if self.regex != self.parameters.regex:
198
+ final_regex = self.parameters.regex or self.regex
199
+ if not final_regex: # falsy parameters.regex override falsy regex
200
+ final_regex = self.parameters.regex
201
+ self.regex = self.parameters.regex = final_regex
202
+ if self.min_length != self.parameters.min_length:
203
+ final_min_length = self.parameters.min_length or self.min_length
204
+ if not final_min_length: # falsy parameters.min_length override falsy min_length
205
+ final_min_length = self.parameters.min_length
206
+ self.min_length = self.parameters.min_length = final_min_length
207
+ if self.max_length != self.parameters.max_length:
208
+ final_max_length = self.parameters.max_length or self.max_length
209
+ if not final_max_length: # falsy parameters.max_length override falsy max_length
210
+ final_max_length = self.parameters.max_length
211
+ self.max_length = self.parameters.max_length = final_max_length
212
+ return self
213
+
214
+ def get_regex(self) -> str | None:
215
+ return self.parameters.regex
216
+
217
+ def get_min_length(self) -> int | None:
218
+ return self.parameters.min_length
219
+
220
+ def get_max_length(self) -> int | None:
221
+ return self.parameters.max_length