infrahub-server 1.2.7__py3-none-any.whl → 1.2.9__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 (85) hide show
  1. infrahub/api/transformation.py +1 -0
  2. infrahub/artifacts/models.py +4 -0
  3. infrahub/cli/db.py +15 -6
  4. infrahub/computed_attribute/tasks.py +34 -12
  5. infrahub/config.py +2 -1
  6. infrahub/constants/__init__.py +0 -0
  7. infrahub/core/branch/tasks.py +0 -2
  8. infrahub/core/constants/__init__.py +1 -0
  9. infrahub/core/diff/calculator.py +4 -3
  10. infrahub/core/diff/combiner.py +1 -2
  11. infrahub/core/diff/coordinator.py +44 -28
  12. infrahub/core/diff/data_check_synchronizer.py +3 -2
  13. infrahub/core/diff/enricher/hierarchy.py +38 -27
  14. infrahub/core/diff/ipam_diff_parser.py +5 -4
  15. infrahub/core/diff/merger/merger.py +20 -18
  16. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  17. infrahub/core/diff/model/path.py +55 -58
  18. infrahub/core/diff/parent_node_adder.py +14 -16
  19. infrahub/core/diff/query/drop_nodes.py +42 -0
  20. infrahub/core/diff/query/field_specifiers.py +8 -7
  21. infrahub/core/diff/query/filters.py +15 -1
  22. infrahub/core/diff/query/save.py +3 -0
  23. infrahub/core/diff/query_parser.py +49 -52
  24. infrahub/core/diff/repository/deserializer.py +36 -23
  25. infrahub/core/diff/repository/repository.py +31 -12
  26. infrahub/core/graph/__init__.py +1 -1
  27. infrahub/core/graph/index.py +3 -1
  28. infrahub/core/initialization.py +23 -7
  29. infrahub/core/manager.py +16 -5
  30. infrahub/core/migrations/graph/__init__.py +2 -0
  31. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +9 -8
  32. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  33. infrahub/core/protocols.py +1 -0
  34. infrahub/core/query/branch.py +27 -17
  35. infrahub/core/query/diff.py +65 -38
  36. infrahub/core/query/node.py +111 -33
  37. infrahub/core/query/relationship.py +17 -3
  38. infrahub/core/query/subquery.py +2 -2
  39. infrahub/core/schema/definitions/core/builtin.py +2 -4
  40. infrahub/core/schema/definitions/core/transform.py +1 -0
  41. infrahub/core/schema/schema_branch.py +3 -0
  42. infrahub/core/validators/aggregated_checker.py +2 -2
  43. infrahub/core/validators/uniqueness/query.py +30 -9
  44. infrahub/database/__init__.py +1 -16
  45. infrahub/database/index.py +1 -1
  46. infrahub/database/memgraph.py +1 -12
  47. infrahub/database/neo4j.py +1 -13
  48. infrahub/git/integrator.py +27 -3
  49. infrahub/git/models.py +4 -0
  50. infrahub/git/tasks.py +3 -0
  51. infrahub/git_credential/helper.py +2 -2
  52. infrahub/graphql/mutations/computed_attribute.py +5 -1
  53. infrahub/graphql/queries/diff/tree.py +2 -1
  54. infrahub/message_bus/operations/requests/proposed_change.py +6 -0
  55. infrahub/message_bus/types.py +3 -0
  56. infrahub/patch/queries/consolidate_duplicated_nodes.py +109 -0
  57. infrahub/patch/queries/delete_duplicated_edges.py +138 -0
  58. infrahub/proposed_change/tasks.py +1 -0
  59. infrahub/server.py +1 -3
  60. infrahub/transformations/models.py +3 -0
  61. infrahub/transformations/tasks.py +1 -0
  62. infrahub/trigger/models.py +11 -1
  63. infrahub/trigger/setup.py +38 -13
  64. infrahub/trigger/tasks.py +1 -4
  65. infrahub/webhook/models.py +3 -0
  66. infrahub/workflows/initialization.py +1 -3
  67. infrahub_sdk/client.py +4 -4
  68. infrahub_sdk/config.py +17 -0
  69. infrahub_sdk/ctl/cli_commands.py +7 -1
  70. infrahub_sdk/ctl/generator.py +2 -2
  71. infrahub_sdk/generator.py +12 -66
  72. infrahub_sdk/operation.py +80 -0
  73. infrahub_sdk/protocols.py +12 -0
  74. infrahub_sdk/recorder.py +3 -0
  75. infrahub_sdk/schema/repository.py +4 -0
  76. infrahub_sdk/transforms.py +15 -27
  77. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/METADATA +2 -2
  78. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/RECORD +84 -78
  79. infrahub_testcontainers/container.py +1 -0
  80. infrahub_testcontainers/docker-compose.test.yml +5 -1
  81. infrahub/database/manager.py +0 -15
  82. /infrahub/{database/constants.py → constants/database.py} +0 -0
  83. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/LICENSE.txt +0 -0
  84. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/WHEEL +0 -0
  85. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/entry_points.txt +0 -0
infrahub/core/manager.py CHANGED
@@ -1229,20 +1229,31 @@ class NodeManager:
1229
1229
  if not prefetch_relationships and not fields:
1230
1230
  return
1231
1231
  cardinality_one_identifiers_by_kind: dict[str, dict[str, RelationshipDirection]] | None = None
1232
- all_identifiers: list[str] | None = None
1232
+ outbound_identifiers: set[str] | None = None
1233
+ inbound_identifiers: set[str] | None = None
1234
+ bidirectional_identifiers: set[str] | None = None
1233
1235
  if not prefetch_relationships:
1234
1236
  cardinality_one_identifiers_by_kind = _get_cardinality_one_identifiers_by_kind(
1235
1237
  nodes=nodes_by_id.values(), fields=fields or {}
1236
1238
  )
1237
- all_identifiers_set: set[str] = set()
1239
+ outbound_identifiers = set()
1240
+ inbound_identifiers = set()
1241
+ bidirectional_identifiers = set()
1238
1242
  for identifier_direction_map in cardinality_one_identifiers_by_kind.values():
1239
- all_identifiers_set.update(identifier_direction_map.keys())
1240
- all_identifiers = list(all_identifiers_set)
1243
+ for identifier, direction in identifier_direction_map.items():
1244
+ if direction is RelationshipDirection.OUTBOUND:
1245
+ outbound_identifiers.add(identifier)
1246
+ elif direction is RelationshipDirection.INBOUND:
1247
+ inbound_identifiers.add(identifier)
1248
+ elif direction is RelationshipDirection.BIDIR:
1249
+ bidirectional_identifiers.add(identifier)
1241
1250
 
1242
1251
  query = await NodeListGetRelationshipsQuery.init(
1243
1252
  db=db,
1244
1253
  ids=list(nodes_by_id.keys()),
1245
- relationship_identifiers=all_identifiers,
1254
+ outbound_identifiers=None if outbound_identifiers is None else list(outbound_identifiers),
1255
+ inbound_identifiers=None if inbound_identifiers is None else list(inbound_identifiers),
1256
+ bidirectional_identifiers=None if bidirectional_identifiers is None else list(bidirectional_identifiers),
1246
1257
  branch=branch,
1247
1258
  at=at,
1248
1259
  branch_agnostic=branch_agnostic,
@@ -28,6 +28,7 @@ from .m023_deduplicate_cardinality_one_relationships import Migration023
28
28
  from .m024_missing_hierarchy_backfill import Migration024
29
29
  from .m025_uniqueness_nulls import Migration025
30
30
  from .m026_0000_prefix_fix import Migration026
31
+ from .m027_delete_isolated_nodes import Migration027
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  from infrahub.core.root import Root
@@ -61,6 +62,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
61
62
  Migration024,
62
63
  Migration025,
63
64
  Migration026,
65
+ Migration027,
64
66
  ]
65
67
 
66
68
 
@@ -2,11 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Sequence
4
4
 
5
+ from infrahub.constants.database import IndexType
5
6
  from infrahub.core.migrations.shared import MigrationResult
6
7
  from infrahub.core.query import Query # noqa: TC001
7
8
  from infrahub.database import DatabaseType
8
- from infrahub.database.constants import IndexType
9
9
  from infrahub.database.index import IndexItem
10
+ from infrahub.database.neo4j import IndexManagerNeo4j
10
11
 
11
12
  from ..shared import GraphMigration
12
13
 
@@ -29,13 +30,13 @@ class Migration014(GraphMigration):
29
30
  if db.db_type != DatabaseType.NEO4J:
30
31
  return result
31
32
 
32
- async with db.start_transaction() as ts:
33
- try:
34
- ts.manager.index.init(nodes=[INDEX_TO_DELETE], rels=[])
35
- await ts.manager.index.drop()
36
- except Exception as exc:
37
- result.errors.append(str(exc))
38
- return result
33
+ try:
34
+ index_manager = IndexManagerNeo4j(db=db)
35
+ index_manager.init(nodes=[INDEX_TO_DELETE], rels=[])
36
+ await index_manager.drop()
37
+ except Exception as exc:
38
+ result.errors.append(str(exc))
39
+ return result
39
40
 
40
41
  return result
41
42
 
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core.migrations.shared import GraphMigration, MigrationResult
6
+ from infrahub.log import get_logger
7
+
8
+ from ...query import Query, QueryType
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.database import InfrahubDatabase
12
+
13
+ log = get_logger()
14
+
15
+
16
+ class DeleteIsolatedNodesQuery(Query):
17
+ name = "delete_isolated_nodes_query"
18
+ type = QueryType.WRITE
19
+ insert_return = False
20
+
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
+ query = """
23
+ MATCH p = (s: Node)-[r]-(d)
24
+ WHERE NOT exists((s)-[:IS_PART_OF]-(:Root))
25
+ DELETE r
26
+
27
+ WITH p
28
+ UNWIND nodes(p) AS n
29
+ MATCH (n)
30
+ WHERE NOT exists((n)--())
31
+ DELETE n
32
+ """
33
+ self.add_to_query(query)
34
+
35
+
36
+ class Migration027(GraphMigration):
37
+ """
38
+ While deleting a branch containing some allocated nodes from a resource pool, relationship
39
+ between pool node and resource node might be agnostic (eg: for IPPrefixPool) and incorrectly deleted,
40
+ resulting in a node still linked to the resource pool but not linked to Root anymore.
41
+ This query deletes nodes not linked to Root and their relationships (supposed to be agnostic).
42
+ """
43
+
44
+ name: str = "027_deleted_isolated_nodes"
45
+ minimum_version: int = 26
46
+ queries: Sequence[type[Query]] = [DeleteIsolatedNodesQuery]
47
+
48
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
49
+ result = MigrationResult()
50
+ return result
@@ -478,6 +478,7 @@ class CoreTransformJinja2(CoreTransformation):
478
478
  class CoreTransformPython(CoreTransformation):
479
479
  file_path: String
480
480
  class_name: String
481
+ convert_query_response: BooleanOptional
481
482
 
482
483
 
483
484
  class CoreUserValidator(CoreValidator):
@@ -51,23 +51,33 @@ class DeleteBranchRelationshipsQuery(Query):
51
51
  super().__init__(**kwargs)
52
52
 
53
53
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
54
- if config.SETTINGS.database.db_type == config.DatabaseType.MEMGRAPH:
55
- query = """
56
- MATCH p = (s)-[r]-(d)
57
- WHERE r.branch = $branch_name
58
- DELETE r
59
- """
60
- else:
61
- query = """
62
- MATCH p = (s)-[r]-(d)
63
- WHERE r.branch = $branch_name
64
- DELETE r
65
- WITH *
66
- UNWIND nodes(p) AS n
67
- MATCH (n)
68
- WHERE NOT exists((n)--())
69
- DELETE n
70
- """
54
+ query = """
55
+ MATCH (s)-[r1]-(d)
56
+ WHERE r1.branch = $branch_name
57
+ DELETE r1
58
+
59
+ WITH collect(DISTINCT s) + collect(DISTINCT d) AS nodes
60
+
61
+ // Collect node IDs for filtering
62
+ WITH nodes, [n in nodes | n.uuid] as nodes_uuids
63
+
64
+ // Also delete agnostic relationships that would not have been deleted above
65
+ MATCH (s2: Node)-[r2]-(d2)
66
+ WHERE NOT exists((s2)-[:IS_PART_OF]-(:Root))
67
+ AND s2.uuid IN nodes_uuids
68
+ DELETE r2
69
+
70
+ WITH nodes, collect(DISTINCT s2) + collect(DISTINCT d2) as additional_nodes
71
+
72
+ WITH nodes + additional_nodes as nodes
73
+
74
+ // Delete nodes that are no longer connected to any other nodes
75
+ UNWIND nodes AS n
76
+ WITH DISTINCT n
77
+ MATCH (n)
78
+ WHERE NOT exists((n)--())
79
+ DELETE n
80
+ """
71
81
  self.params["branch_name"] = self.branch_name
72
82
  self.add_to_query(query)
73
83
 
@@ -9,6 +9,7 @@ from infrahub.core.timestamp import Timestamp
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from infrahub.core.branch import Branch
12
+ from infrahub.core.diff.model.field_specifiers_map import NodeFieldSpecifierMap
12
13
  from infrahub.database import InfrahubDatabase
13
14
 
14
15
 
@@ -106,8 +107,8 @@ class DiffCalculationQuery(DiffQuery):
106
107
  self,
107
108
  base_branch: Branch,
108
109
  diff_branch_from_time: Timestamp,
109
- current_node_field_specifiers: dict[str, set[str]] | None = None,
110
- new_node_field_specifiers: dict[str, set[str]] | None = None,
110
+ current_node_field_specifiers: NodeFieldSpecifierMap | None = None,
111
+ new_node_field_specifiers: NodeFieldSpecifierMap | None = None,
111
112
  **kwargs: Any,
112
113
  ):
113
114
  self.base_branch = base_branch
@@ -231,10 +232,10 @@ class DiffNodePathsQuery(DiffCalculationQuery):
231
232
  self.params.update(params_dict)
232
233
  self.params.update(
233
234
  {
234
- "new_node_ids_list": list(self.new_node_field_specifiers.keys())
235
+ "new_node_ids_list": self.new_node_field_specifiers.get_uuids_list()
235
236
  if self.new_node_field_specifiers
236
237
  else None,
237
- "current_node_ids_list": list(self.current_node_field_specifiers.keys())
238
+ "current_node_ids_list": self.current_node_field_specifiers.get_uuids_list()
238
239
  if self.current_node_field_specifiers
239
240
  else None,
240
241
  }
@@ -371,15 +372,16 @@ class DiffFieldPathsQuery(DiffCalculationQuery):
371
372
 
372
373
  self.params.update(
373
374
  {
374
- "current_node_field_specifiers_map": {
375
- node_uuid: list(field_names)
376
- for node_uuid, field_names in self.current_node_field_specifiers.items()
377
- }
375
+ "current_node_ids_list": self.current_node_field_specifiers.get_uuids_list()
376
+ if self.current_node_field_specifiers
377
+ else None,
378
+ "new_node_ids_list": self.new_node_field_specifiers.get_uuids_list()
379
+ if self.new_node_field_specifiers
380
+ else None,
381
+ "current_node_field_specifiers_map": self.current_node_field_specifiers.get_uuid_field_names_map()
378
382
  if self.current_node_field_specifiers is not None
379
383
  else None,
380
- "new_node_field_specifiers_map": {
381
- node_uuid: list(field_names) for node_uuid, field_names in self.new_node_field_specifiers.items()
382
- }
384
+ "new_node_field_specifiers_map": self.new_node_field_specifiers.get_uuid_field_names_map()
383
385
  if self.new_node_field_specifiers is not None
384
386
  else None,
385
387
  }
@@ -400,16 +402,16 @@ AND (r_root.to IS NULL OR diff_rel.branch <> r_root.branch OR r_root.to >= diff_
400
402
  // node ID and field name filtering first pass
401
403
  AND (
402
404
  (
403
- $current_node_field_specifiers_map IS NOT NULL
404
- AND $current_node_field_specifiers_map[p.uuid] IS NOT NULL
405
+ $current_node_ids_list IS NOT NULL
406
+ AND p.uuid IN $current_node_ids_list
405
407
  AND q.name IN $current_node_field_specifiers_map[p.uuid]
406
408
  ) OR (
407
- $new_node_field_specifiers_map IS NOT NULL
408
- AND $new_node_field_specifiers_map[p.uuid] IS NOT NULL
409
+ $new_node_ids_list IS NOT NULL
410
+ AND p.uuid IN $new_node_ids_list
409
411
  AND q.name IN $new_node_field_specifiers_map[p.uuid]
410
412
  ) OR (
411
- $current_node_field_specifiers_map IS NULL
412
- AND $new_node_field_specifiers_map IS NULL
413
+ $new_node_ids_list IS NULL
414
+ AND $current_node_ids_list IS NULL
413
415
  )
414
416
  )
415
417
  // node ID and field name filtering second pass
@@ -417,8 +419,12 @@ AND (
417
419
  // time-based filters for nodes already included in the diff or fresh changes
418
420
  (
419
421
  (
420
- ($current_node_field_specifiers_map IS NOT NULL AND q.name IN $current_node_field_specifiers_map[p.uuid])
421
- OR ($current_node_field_specifiers_map IS NULL AND $new_node_field_specifiers_map IS NULL)
422
+ (
423
+ $current_node_ids_list IS NOT NULL
424
+ AND p.uuid IN $current_node_ids_list
425
+ AND q.name IN $current_node_field_specifiers_map[p.uuid]
426
+ )
427
+ OR ($current_node_ids_list IS NULL AND $new_node_ids_list IS NULL)
422
428
  )
423
429
  AND (r_root.from < $from_time OR p.branch_support = $branch_agnostic)
424
430
  AND (
@@ -428,7 +434,11 @@ AND (
428
434
  )
429
435
  // time-based filters for new nodes
430
436
  OR (
431
- ($new_node_field_specifiers_map IS NOT NULL AND q.name IN $new_node_field_specifiers_map[p.uuid])
437
+ (
438
+ $new_node_ids_list IS NOT NULL
439
+ AND p.uuid IN $new_node_ids_list
440
+ AND q.name IN $new_node_field_specifiers_map[p.uuid]
441
+ )
432
442
  AND (r_root.from < $branch_from_time OR p.branch_support = $branch_agnostic)
433
443
  AND (
434
444
  ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
@@ -454,7 +464,11 @@ WITH one_result[0] AS root, one_result[1] AS r_root, one_result[2] AS p, one_res
454
464
  // Add correct from_time for row
455
465
  // -------------------------------------
456
466
  WITH root, r_root, p, diff_rel, q, has_more_data, CASE
457
- WHEN $new_node_field_specifiers_map IS NOT NULL AND q.name IN $new_node_field_specifiers_map[p.uuid] THEN $branch_from_time
467
+ WHEN
468
+ $new_node_ids_list IS NOT NULL
469
+ AND p.uuid IN $new_node_ids_list
470
+ AND q.name IN $new_node_field_specifiers_map[p.uuid]
471
+ THEN $branch_from_time
458
472
  ELSE $from_time
459
473
  END AS row_from_time
460
474
  // -------------------------------------
@@ -554,15 +568,16 @@ class DiffPropertyPathsQuery(DiffCalculationQuery):
554
568
 
555
569
  self.params.update(
556
570
  {
557
- "current_node_field_specifiers_map": {
558
- node_uuid: list(field_names)
559
- for node_uuid, field_names in self.current_node_field_specifiers.items()
560
- }
571
+ "current_node_ids_list": self.current_node_field_specifiers.get_uuids_list()
572
+ if self.current_node_field_specifiers
573
+ else None,
574
+ "new_node_ids_list": self.new_node_field_specifiers.get_uuids_list()
575
+ if self.new_node_field_specifiers
576
+ else None,
577
+ "current_node_field_specifiers_map": self.current_node_field_specifiers.get_uuid_field_names_map()
561
578
  if self.current_node_field_specifiers is not None
562
579
  else None,
563
- "new_node_field_specifiers_map": {
564
- node_uuid: list(field_names) for node_uuid, field_names in self.new_node_field_specifiers.items()
565
- }
580
+ "new_node_field_specifiers_map": self.new_node_field_specifiers.get_uuid_field_names_map()
566
581
  if self.new_node_field_specifiers is not None
567
582
  else None,
568
583
  }
@@ -580,16 +595,16 @@ AND type(r_node) IN ["HAS_ATTRIBUTE", "IS_RELATED"]
580
595
  // node ID and field name filtering first pass
581
596
  AND (
582
597
  (
583
- $current_node_field_specifiers_map IS NOT NULL
584
- AND $current_node_field_specifiers_map[n.uuid] IS NOT NULL
598
+ $current_node_ids_list IS NOT NULL
599
+ AND n.uuid IN $current_node_ids_list
585
600
  AND p.name IN $current_node_field_specifiers_map[n.uuid]
586
601
  ) OR (
587
- $new_node_field_specifiers_map IS NOT NULL
588
- AND $new_node_field_specifiers_map[n.uuid] IS NOT NULL
602
+ $new_node_ids_list IS NOT NULL
603
+ AND n.uuid IN $new_node_ids_list
589
604
  AND p.name IN $new_node_field_specifiers_map[n.uuid]
590
605
  ) OR (
591
- $current_node_field_specifiers_map IS NULL
592
- AND $new_node_field_specifiers_map IS NULL
606
+ $new_node_ids_list IS NULL
607
+ AND $current_node_ids_list IS NULL
593
608
  )
594
609
  )
595
610
  // node ID and field name filtering second pass
@@ -597,8 +612,12 @@ AND (
597
612
  // time-based filters for nodes already included in the diff or fresh changes
598
613
  (
599
614
  (
600
- ($current_node_field_specifiers_map IS NOT NULL AND p.name IN $current_node_field_specifiers_map[n.uuid])
601
- OR ($current_node_field_specifiers_map IS NULL AND $new_node_field_specifiers_map IS NULL)
615
+ (
616
+ $current_node_ids_list IS NOT NULL
617
+ AND n.uuid IN $current_node_ids_list
618
+ AND p.name IN $current_node_field_specifiers_map[n.uuid]
619
+ )
620
+ OR ($current_node_ids_list IS NULL AND $new_node_ids_list IS NULL)
602
621
  )
603
622
  AND (
604
623
  ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
@@ -612,7 +631,11 @@ AND (
612
631
  )
613
632
  // time-based filters for new nodes
614
633
  OR (
615
- ($new_node_field_specifiers_map IS NOT NULL AND p.name IN $new_node_field_specifiers_map[n.uuid])
634
+ (
635
+ $new_node_ids_list IS NOT NULL
636
+ AND n.uuid IN $new_node_ids_list
637
+ AND p.name IN $new_node_field_specifiers_map[n.uuid]
638
+ )
616
639
  AND (
617
640
  ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
618
641
  OR ($branch_from_time <= diff_rel.to < $to_time)
@@ -667,7 +690,11 @@ WITH one_result[0] AS diff_rel_path, one_result[1] AS r_root, one_result[2] AS n
667
690
  // Add correct from_time for row
668
691
  // -------------------------------------
669
692
  WITH diff_rel_path, r_root, n, r_node, p, diff_rel, has_more_data, CASE
670
- WHEN $new_node_field_specifiers_map IS NOT NULL AND p.name IN $new_node_field_specifiers_map[n.uuid] THEN $branch_from_time
693
+ WHEN
694
+ $new_node_ids_list IS NOT NULL
695
+ AND n.uuid IN $new_node_ids_list
696
+ AND p.name IN $new_node_field_specifiers_map[n.uuid]
697
+ THEN $branch_from_time
671
698
  ELSE $from_time
672
699
  END AS row_from_time
673
700
  WITH diff_rel_path, r_root, n, r_node, p, diff_rel, has_more_data, row_from_time
@@ -92,6 +92,7 @@ class NodeAttributesFromDB:
92
92
  class PeerInfo:
93
93
  uuid: str
94
94
  kind: str
95
+ labels: frozenset[str]
95
96
 
96
97
 
97
98
  class NodeQuery(Query):
@@ -649,51 +650,118 @@ class NodeListGetRelationshipsQuery(Query):
649
650
  type: QueryType = QueryType.READ
650
651
  insert_return: bool = False
651
652
 
652
- def __init__(self, ids: list[str], relationship_identifiers: list[str] | None = None, **kwargs):
653
+ def __init__(
654
+ self,
655
+ ids: list[str],
656
+ outbound_identifiers: list[str] | None = None,
657
+ inbound_identifiers: list[str] | None = None,
658
+ bidirectional_identifiers: list[str] | None = None,
659
+ **kwargs,
660
+ ):
653
661
  self.ids = ids
654
- self.relationship_identifiers = relationship_identifiers
662
+ self.outbound_identifiers = outbound_identifiers
663
+ self.inbound_identifiers = inbound_identifiers
664
+ self.bidirectional_identifiers = bidirectional_identifiers
655
665
  super().__init__(**kwargs)
656
666
 
657
667
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
658
668
  self.params["ids"] = self.ids
659
- self.params["relationship_identifiers"] = self.relationship_identifiers
669
+ self.params["outbound_identifiers"] = self.outbound_identifiers
670
+ self.params["inbound_identifiers"] = self.inbound_identifiers
671
+ self.params["bidirectional_identifiers"] = self.bidirectional_identifiers
660
672
 
661
673
  rels_filter, rels_params = self.branch.get_query_filter_path(at=self.at, branch_agnostic=self.branch_agnostic)
662
674
  self.params.update(rels_params)
663
675
 
664
676
  query = """
665
677
  MATCH (n:Node) WHERE n.uuid IN $ids
666
- MATCH paths_in = ((n)<-[r1:IS_RELATED]-(rel:Relationship)<-[r2:IS_RELATED]-(peer))
667
- WHERE ($relationship_identifiers IS NULL OR rel.name in $relationship_identifiers)
668
- AND all(r IN relationships(paths_in) WHERE (%(filters)s))
669
- AND n.uuid <> peer.uuid
670
- RETURN n, rel, peer, r1, r2, "inbound" as direction
671
- UNION
672
- MATCH (n:Node) WHERE n.uuid IN $ids
673
- MATCH paths_out = ((n)-[r1:IS_RELATED]->(rel:Relationship)-[r2:IS_RELATED]->(peer))
674
- WHERE ($relationship_identifiers IS NULL OR rel.name in $relationship_identifiers)
675
- AND all(r IN relationships(paths_out) WHERE (%(filters)s))
676
- AND n.uuid <> peer.uuid
677
- RETURN n, rel, peer, r1, r2, "outbound" as direction
678
- UNION
679
- MATCH (n:Node) WHERE n.uuid IN $ids
680
- MATCH paths_bidir = ((n)-[r1:IS_RELATED]->(rel:Relationship)<-[r2:IS_RELATED]-(peer))
681
- WHERE ($relationship_identifiers IS NULL OR rel.name in $relationship_identifiers)
682
- AND all(r IN relationships(paths_bidir) WHERE (%(filters)s))
683
- AND n.uuid <> peer.uuid
684
- RETURN n, rel, peer, r1, r2, "bidirectional" as direction
678
+ CALL {
679
+ WITH n
680
+ MATCH (n)<-[:IS_RELATED]-(rel:Relationship)<-[:IS_RELATED]-(peer)
681
+ WHERE ($inbound_identifiers IS NULL OR rel.name in $inbound_identifiers)
682
+ AND n.uuid <> peer.uuid
683
+ WITH DISTINCT n, rel, peer
684
+ CALL {
685
+ WITH n, rel, peer
686
+ MATCH (n)<-[r:IS_RELATED]-(rel)
687
+ WHERE (%(filters)s)
688
+ WITH n, rel, peer, r
689
+ ORDER BY r.from DESC
690
+ LIMIT 1
691
+ WITH n, rel, peer, r AS r1
692
+ WHERE r1.status = "active"
693
+ MATCH (rel)<-[r:IS_RELATED]-(peer)
694
+ WHERE (%(filters)s)
695
+ WITH r1, r
696
+ ORDER BY r.from DESC
697
+ LIMIT 1
698
+ WITH r1, r AS r2
699
+ WHERE r2.status = "active"
700
+ RETURN 1 AS is_active
701
+ }
702
+ RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "inbound" as direction
703
+ UNION
704
+ WITH n
705
+ MATCH (n)-[:IS_RELATED]->(rel:Relationship)-[:IS_RELATED]->(peer)
706
+ WHERE ($outbound_identifiers IS NULL OR rel.name in $outbound_identifiers)
707
+ AND n.uuid <> peer.uuid
708
+ WITH DISTINCT n, rel, peer
709
+ CALL {
710
+ WITH n, rel, peer
711
+ MATCH (n)-[r:IS_RELATED]->(rel)
712
+ WHERE (%(filters)s)
713
+ WITH n, rel, peer, r
714
+ ORDER BY r.from DESC
715
+ LIMIT 1
716
+ WITH n, rel, peer, r AS r1
717
+ WHERE r1.status = "active"
718
+ MATCH (rel)-[r:IS_RELATED]->(peer)
719
+ WHERE (%(filters)s)
720
+ WITH r1, r
721
+ ORDER BY r.from DESC
722
+ LIMIT 1
723
+ WITH r1, r AS r2
724
+ WHERE r2.status = "active"
725
+ RETURN 1 AS is_active
726
+ }
727
+ RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "outbound" as direction
728
+ UNION
729
+ WITH n
730
+ MATCH (n)-[:IS_RELATED]->(rel:Relationship)<-[:IS_RELATED]-(peer)
731
+ WHERE ($bidirectional_identifiers IS NULL OR rel.name in $bidirectional_identifiers)
732
+ AND n.uuid <> peer.uuid
733
+ WITH DISTINCT n, rel, peer
734
+ CALL {
735
+ WITH n, rel, peer
736
+ MATCH (n)-[r:IS_RELATED]->(rel)
737
+ WHERE (%(filters)s)
738
+ WITH n, rel, peer, r
739
+ ORDER BY r.from DESC
740
+ LIMIT 1
741
+ WITH n, rel, peer, r AS r1
742
+ WHERE r1.status = "active"
743
+ MATCH (rel)<-[r:IS_RELATED]-(peer)
744
+ WHERE (%(filters)s)
745
+ WITH r1, r
746
+ ORDER BY r.from DESC
747
+ LIMIT 1
748
+ WITH r1, r AS r2
749
+ WHERE r2.status = "active"
750
+ RETURN 1 AS is_active
751
+ }
752
+ RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "bidirectional" as direction
753
+ }
754
+ RETURN DISTINCT n_uuid, rel_name, peer_uuid, direction
685
755
  """ % {"filters": rels_filter}
686
-
687
756
  self.add_to_query(query)
688
-
689
- self.return_labels = ["n", "rel", "peer", "r1", "r2", "direction"]
757
+ self.return_labels = ["n_uuid", "rel_name", "peer_uuid", "direction"]
690
758
 
691
759
  def get_peers_group_by_node(self) -> GroupedPeerNodes:
692
760
  gpn = GroupedPeerNodes()
693
- for result in self.get_results_group_by(("n", "uuid"), ("rel", "name"), ("peer", "uuid")):
694
- node_id = result.get("n").get("uuid")
695
- rel_name = result.get("rel").get("name")
696
- peer_id = result.get("peer").get("uuid")
761
+ for result in self.get_results():
762
+ node_id = result.get("n_uuid")
763
+ rel_name = result.get("rel_name")
764
+ peer_id = result.get("peer_uuid")
697
765
  direction = str(result.get("direction"))
698
766
  direction_enum = {
699
767
  "inbound": RelationshipDirection.INBOUND,
@@ -1338,7 +1406,7 @@ class NodeGetHierarchyQuery(Query):
1338
1406
 
1339
1407
  super().__init__(**kwargs)
1340
1408
 
1341
- async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
1409
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002,PLR0915
1342
1410
  hierarchy_schema = self.node_schema.get_hierarchy_schema(db=db, branch=self.branch)
1343
1411
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
1344
1412
  self.params.update(branch_params)
@@ -1371,6 +1439,10 @@ class NodeGetHierarchyQuery(Query):
1371
1439
  UNWIND peers_with_duplicates AS pwd
1372
1440
  RETURN DISTINCT pwd AS peer
1373
1441
  }
1442
+ """ % {"filter": filter_str, "branch_filter": branch_filter}
1443
+
1444
+ if not self.branch.is_default:
1445
+ query += """
1374
1446
  CALL {
1375
1447
  WITH n, peer
1376
1448
  MATCH path = (n)%(filter)s(peer)
@@ -1381,10 +1453,14 @@ class NodeGetHierarchyQuery(Query):
1381
1453
  LIMIT 1
1382
1454
  }
1383
1455
  WITH peer1 as peer, is_active
1384
- """ % {"filter": filter_str, "branch_filter": branch_filter, "with_clause": with_clause}
1456
+ """ % {"filter": filter_str, "branch_filter": branch_filter, "with_clause": with_clause}
1457
+ else:
1458
+ query += """
1459
+ WITH peer
1460
+ """
1385
1461
 
1386
1462
  self.add_to_query(query)
1387
- where_clause = ["is_active = TRUE"]
1463
+ where_clause = ["is_active = TRUE"] if not self.branch.is_default else []
1388
1464
 
1389
1465
  clean_filters = extract_field_filters(field_name=self.direction.value, filters=self.filters)
1390
1466
 
@@ -1394,7 +1470,8 @@ class NodeGetHierarchyQuery(Query):
1394
1470
  if clean_filters.get("id", None):
1395
1471
  self.params["peer_ids"].append(clean_filters.get("id"))
1396
1472
 
1397
- self.add_to_query("WHERE " + " AND ".join(where_clause))
1473
+ if where_clause:
1474
+ self.add_to_query("WHERE " + " AND ".join(where_clause))
1398
1475
 
1399
1476
  self.return_labels = ["peer"]
1400
1477
 
@@ -1477,4 +1554,5 @@ class NodeGetHierarchyQuery(Query):
1477
1554
  yield PeerInfo(
1478
1555
  uuid=peer_node.get("uuid"),
1479
1556
  kind=peer_node.get("kind"),
1557
+ labels=peer_node.labels,
1480
1558
  )
@@ -73,12 +73,21 @@ class RelationshipPeerData:
73
73
  source_id: UUID
74
74
  """UUID of the Source Node."""
75
75
 
76
+ source_kind: str
77
+ """Kind of the Source Node."""
78
+
79
+ source_labels: frozenset[str]
80
+ """Labels of the Source Node."""
81
+
76
82
  peer_id: UUID
77
83
  """UUID of the Peer Node."""
78
84
 
79
85
  peer_kind: str
80
86
  """Kind of the Peer Node."""
81
87
 
88
+ peer_labels: frozenset[str]
89
+ """Labels of the Peer Node."""
90
+
82
91
  properties: dict[str, FlagPropertyData | NodePropertyData]
83
92
  """UUID of the Relationship Node."""
84
93
 
@@ -752,10 +761,15 @@ class RelationshipGetPeerQuery(Query):
752
761
  def get_peers(self) -> Generator[RelationshipPeerData, None, None]:
753
762
  for result in self.get_results_group_by(("peer", "uuid"), ("source_node", "uuid")):
754
763
  rels = result.get("rels")
764
+ source_node = result.get_node("source_node")
765
+ peer_node = result.get_node("peer")
755
766
  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"),
767
+ source_id=source_node.get("uuid"),
768
+ source_kind=source_node.get("kind"),
769
+ source_labels=source_node.labels,
770
+ peer_id=peer_node.get("uuid"),
771
+ peer_kind=peer_node.get("kind"),
772
+ peer_labels=peer_node.labels,
759
773
  rel_node_db_id=result.get("rl").element_id,
760
774
  rel_node_id=result.get("rl").get("uuid"),
761
775
  updated_at=rels[0]["from"],