infrahub-server 1.6.3__py3-none-any.whl → 1.7.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 (161) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/schema.py +3 -1
  3. infrahub/artifacts/tasks.py +1 -0
  4. infrahub/auth.py +2 -2
  5. infrahub/cli/db.py +6 -6
  6. infrahub/computed_attribute/gather.py +3 -4
  7. infrahub/computed_attribute/tasks.py +23 -6
  8. infrahub/config.py +8 -0
  9. infrahub/constants/enums.py +12 -0
  10. infrahub/core/account.py +5 -8
  11. infrahub/core/attribute.py +106 -108
  12. infrahub/core/branch/models.py +44 -71
  13. infrahub/core/branch/tasks.py +5 -3
  14. infrahub/core/changelog/diff.py +1 -20
  15. infrahub/core/changelog/models.py +0 -7
  16. infrahub/core/constants/__init__.py +17 -0
  17. infrahub/core/constants/database.py +0 -1
  18. infrahub/core/constants/schema.py +0 -1
  19. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  20. infrahub/core/diff/data_check_synchronizer.py +3 -2
  21. infrahub/core/diff/enricher/cardinality_one.py +1 -1
  22. infrahub/core/diff/merger/merger.py +27 -1
  23. infrahub/core/diff/merger/serializer.py +3 -10
  24. infrahub/core/diff/model/diff.py +1 -1
  25. infrahub/core/diff/query/merge.py +376 -135
  26. infrahub/core/graph/__init__.py +1 -1
  27. infrahub/core/graph/constraints.py +2 -2
  28. infrahub/core/graph/schema.py +2 -12
  29. infrahub/core/manager.py +132 -126
  30. infrahub/core/metadata/__init__.py +0 -0
  31. infrahub/core/metadata/interface.py +37 -0
  32. infrahub/core/metadata/model.py +31 -0
  33. infrahub/core/metadata/query/__init__.py +0 -0
  34. infrahub/core/metadata/query/node_metadata.py +301 -0
  35. infrahub/core/migrations/graph/__init__.py +4 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +3 -8
  37. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  38. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  39. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  40. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  41. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  42. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  43. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  44. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  45. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  46. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +38 -0
  47. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  48. infrahub/core/migrations/query/attribute_add.py +17 -6
  49. infrahub/core/migrations/query/attribute_remove.py +19 -5
  50. infrahub/core/migrations/query/attribute_rename.py +21 -5
  51. infrahub/core/migrations/query/node_duplicate.py +19 -4
  52. infrahub/core/migrations/schema/attribute_kind_update.py +25 -7
  53. infrahub/core/migrations/schema/attribute_supports_profile.py +3 -1
  54. infrahub/core/migrations/schema/models.py +3 -0
  55. infrahub/core/migrations/schema/node_attribute_add.py +4 -1
  56. infrahub/core/migrations/schema/node_remove.py +24 -2
  57. infrahub/core/migrations/schema/tasks.py +4 -1
  58. infrahub/core/migrations/shared.py +13 -6
  59. infrahub/core/models.py +6 -6
  60. infrahub/core/node/__init__.py +156 -57
  61. infrahub/core/node/create.py +7 -3
  62. infrahub/core/node/standard.py +100 -14
  63. infrahub/core/property.py +0 -1
  64. infrahub/core/protocols_base.py +6 -2
  65. infrahub/core/query/__init__.py +6 -7
  66. infrahub/core/query/attribute.py +161 -46
  67. infrahub/core/query/branch.py +57 -69
  68. infrahub/core/query/diff.py +4 -4
  69. infrahub/core/query/node.py +618 -180
  70. infrahub/core/query/relationship.py +449 -300
  71. infrahub/core/query/standard_node.py +25 -5
  72. infrahub/core/query/utils.py +2 -4
  73. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  74. infrahub/core/relationship/model.py +293 -139
  75. infrahub/core/schema/attribute_parameters.py +1 -28
  76. infrahub/core/schema/attribute_schema.py +17 -11
  77. infrahub/core/schema/manager.py +63 -43
  78. infrahub/core/schema/relationship_schema.py +6 -2
  79. infrahub/core/schema/schema_branch.py +48 -76
  80. infrahub/core/task/task.py +4 -2
  81. infrahub/core/utils.py +0 -22
  82. infrahub/core/validators/attribute/kind.py +2 -5
  83. infrahub/core/validators/determiner.py +3 -3
  84. infrahub/database/__init__.py +3 -3
  85. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  86. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  87. infrahub/dependencies/registry.py +2 -0
  88. infrahub/display_labels/tasks.py +12 -3
  89. infrahub/git/integrator.py +18 -18
  90. infrahub/git/tasks.py +1 -1
  91. infrahub/graphql/app.py +2 -2
  92. infrahub/graphql/constants.py +3 -0
  93. infrahub/graphql/context.py +1 -1
  94. infrahub/graphql/initialization.py +11 -0
  95. infrahub/graphql/loaders/account.py +134 -0
  96. infrahub/graphql/loaders/node.py +5 -12
  97. infrahub/graphql/loaders/peers.py +5 -7
  98. infrahub/graphql/manager.py +158 -18
  99. infrahub/graphql/metadata.py +91 -0
  100. infrahub/graphql/models.py +33 -3
  101. infrahub/graphql/mutations/account.py +5 -5
  102. infrahub/graphql/mutations/attribute.py +0 -2
  103. infrahub/graphql/mutations/branch.py +9 -5
  104. infrahub/graphql/mutations/computed_attribute.py +1 -1
  105. infrahub/graphql/mutations/display_label.py +1 -1
  106. infrahub/graphql/mutations/hfid.py +1 -1
  107. infrahub/graphql/mutations/ipam.py +4 -6
  108. infrahub/graphql/mutations/main.py +9 -4
  109. infrahub/graphql/mutations/profile.py +16 -22
  110. infrahub/graphql/mutations/proposed_change.py +4 -4
  111. infrahub/graphql/mutations/relationship.py +40 -10
  112. infrahub/graphql/mutations/repository.py +14 -12
  113. infrahub/graphql/mutations/schema.py +2 -2
  114. infrahub/graphql/queries/branch.py +62 -6
  115. infrahub/graphql/queries/diff/tree.py +5 -5
  116. infrahub/graphql/resolvers/account_metadata.py +84 -0
  117. infrahub/graphql/resolvers/ipam.py +6 -8
  118. infrahub/graphql/resolvers/many_relationship.py +77 -35
  119. infrahub/graphql/resolvers/resolver.py +16 -12
  120. infrahub/graphql/resolvers/single_relationship.py +87 -23
  121. infrahub/graphql/subscription/graphql_query.py +2 -0
  122. infrahub/graphql/types/__init__.py +0 -1
  123. infrahub/graphql/types/attribute.py +10 -5
  124. infrahub/graphql/types/branch.py +40 -53
  125. infrahub/graphql/types/enums.py +3 -0
  126. infrahub/graphql/types/metadata.py +28 -0
  127. infrahub/graphql/types/node.py +22 -2
  128. infrahub/graphql/types/relationship.py +10 -2
  129. infrahub/graphql/types/standard_node.py +4 -3
  130. infrahub/hfid/tasks.py +12 -3
  131. infrahub/profiles/gather.py +56 -0
  132. infrahub/profiles/mandatory_fields_checker.py +116 -0
  133. infrahub/profiles/models.py +66 -0
  134. infrahub/profiles/node_applier.py +153 -12
  135. infrahub/profiles/queries/get_profile_data.py +143 -31
  136. infrahub/profiles/tasks.py +79 -27
  137. infrahub/profiles/triggers.py +22 -0
  138. infrahub/proposed_change/tasks.py +4 -1
  139. infrahub/tasks/artifact.py +1 -0
  140. infrahub/transformations/tasks.py +2 -2
  141. infrahub/trigger/catalogue.py +2 -0
  142. infrahub/trigger/models.py +1 -0
  143. infrahub/trigger/setup.py +3 -3
  144. infrahub/trigger/tasks.py +3 -0
  145. infrahub/validators/tasks.py +1 -0
  146. infrahub/webhook/models.py +1 -1
  147. infrahub/webhook/tasks.py +1 -1
  148. infrahub/workers/dependencies.py +9 -3
  149. infrahub/workers/infrahub_async.py +13 -4
  150. infrahub/workflows/catalogue.py +19 -0
  151. infrahub_sdk/node/constants.py +1 -0
  152. infrahub_sdk/node/related_node.py +13 -4
  153. infrahub_sdk/node/relationship.py +8 -0
  154. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/METADATA +17 -16
  155. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/RECORD +161 -143
  156. infrahub_testcontainers/container.py +3 -3
  157. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  158. infrahub_testcontainers/docker-compose.test.yml +13 -5
  159. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/WHEEL +0 -0
  160. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/entry_points.txt +0 -0
  161. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -12,7 +12,7 @@ from infrahub.core.changelog.models import (
12
12
  RelationshipCardinalityManyChangelog,
13
13
  RelationshipCardinalityOneChangelog,
14
14
  )
15
- from infrahub.core.constants import RelationshipDirection, RelationshipStatus
15
+ from infrahub.core.constants import InfrahubKind, MetadataOptions, RelationshipDirection, RelationshipStatus
16
16
  from infrahub.core.constants.database import DatabaseEdgeType
17
17
  from infrahub.core.query import Query, QueryType
18
18
  from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
@@ -91,7 +91,7 @@ class RelationshipPeerData:
91
91
  properties: dict[str, FlagPropertyData | NodePropertyData]
92
92
  """UUID of the Relationship Node."""
93
93
 
94
- rel_node_id: UUID | None = None
94
+ rel_node_id: UUID
95
95
  """UUID of the Relationship Node."""
96
96
 
97
97
  rel_node_db_id: str | None = None
@@ -100,7 +100,13 @@ class RelationshipPeerData:
100
100
  rels: list[RelData] | None = None
101
101
  """Both relationships pointing at this Relationship Node."""
102
102
 
103
- updated_at: str | None = None
103
+ created_at: Timestamp | None = None
104
+ created_by: str | None = None
105
+ updated_at: Timestamp | None = None
106
+ updated_by: str | None = None
107
+
108
+ is_from_profile: bool = False
109
+ profile_id: UUID | None = None
104
110
 
105
111
  def rel_ids_per_branch(self) -> dict[str, list[str | int]]:
106
112
  response = defaultdict(list)
@@ -144,7 +150,7 @@ class RelationshipQuery(Query):
144
150
  def __init__(
145
151
  self,
146
152
  rel: type[Relationship] | Relationship | None = None,
147
- rel_type: str | None = None,
153
+ rel_id: str | None = None,
148
154
  source: Node | None = None,
149
155
  source_id: UUID | None = None,
150
156
  destination: Node | None = None,
@@ -156,10 +162,12 @@ class RelationshipQuery(Query):
156
162
  ):
157
163
  if not source and not source_id:
158
164
  raise ValueError("Either source or source_id must be provided.")
159
- if not rel and not rel_type:
160
- raise ValueError("Either rel or rel_type must be provided.")
161
- if not inspect.isclass(rel) and not hasattr(rel, "schema"):
162
- raise ValueError("Rel must be a Relationship class or an instance of Relationship.")
165
+ if not rel and not rel_id:
166
+ raise ValueError("rel or rel_id must be provided.")
167
+ if not inspect.isclass(rel) and not hasattr(rel, "schema") and not rel_id:
168
+ raise ValueError(
169
+ "Rel must be a Relationship class or an instance of Relationship or a relationship ID must be provided."
170
+ )
163
171
  if not schema and inspect.isclass(rel) and not hasattr(rel, "schema"):
164
172
  raise ValueError("Either an instance of Relationship or a valid schema must be provided.")
165
173
 
@@ -174,7 +182,7 @@ class RelationshipQuery(Query):
174
182
  self.destination_id = destination.id
175
183
 
176
184
  self.rel = rel
177
- self.rel_type = rel_type or self.rel.rel_type
185
+ self.rel_id = rel_id
178
186
  self.schema = schema or self.rel.schema
179
187
 
180
188
  if not branch and inspect.isclass(rel) and not hasattr(rel, "branch"):
@@ -191,12 +199,13 @@ class RelationshipQuery(Query):
191
199
 
192
200
  super().__init__(**kwargs)
193
201
 
194
- def get_relationship_properties_dict(self, status: RelationshipStatus) -> dict[str, str | None]:
202
+ def get_relationship_properties_dict(self, status: RelationshipStatus, user_id: str) -> dict[str, str | None]:
195
203
  rel_prop_dict = {
196
204
  "branch": self.branch.name,
197
205
  "branch_level": self.branch.hierarchy_level,
198
206
  "status": status.value,
199
207
  "from": self.at.to_string(),
208
+ "from_user_id": user_id,
200
209
  }
201
210
  if self.schema.hierarchical:
202
211
  rel_prop_dict["hierarchy"] = self.schema.hierarchical
@@ -206,13 +215,12 @@ class RelationshipQuery(Query):
206
215
  self.params["source_id"] = self.source_id or self.source.get_id()
207
216
  if source_branch.is_global or source_branch.is_default:
208
217
  source_query_match = """
209
- MATCH (s:Node { uuid: $source_id })-[source_e:IS_PART_OF {branch: $source_branch, status: "active"}]->(:Root)
218
+ MATCH (s:Node { uuid: $source_id })-[source_e:IS_PART_OF {branch_level: 1, status: "active"}]->(:Root)
210
219
  WHERE source_e.from <= $at AND (source_e.to IS NULL OR source_e.to > $at)
211
- OPTIONAL MATCH (s)-[delete_edge:IS_PART_OF {status: "deleted", branch: $source_branch}]->(:Root)
220
+ OPTIONAL MATCH (s)-[delete_edge:IS_PART_OF {status: "deleted", branch_level: 1}]->(:Root)
212
221
  WHERE delete_edge.from <= $at
213
222
  WITH *, s WHERE delete_edge IS NULL
214
223
  """
215
- self.params["source_branch"] = source_branch.name
216
224
  else:
217
225
  source_filter, source_filter_params = source_branch.get_query_filter_path(
218
226
  at=self.at, variable_name="r", params_prefix="src_"
@@ -235,13 +243,12 @@ class RelationshipQuery(Query):
235
243
  self.params["destination_id"] = destination_id
236
244
  if destination_branch.is_global or destination_branch.is_default:
237
245
  destination_query_match = """
238
- MATCH (d:Node { uuid: $destination_id })-[dest_e:IS_PART_OF {branch: $destination_branch, status: "active"}]->(:Root)
246
+ MATCH (d:Node { uuid: $destination_id })-[dest_e:IS_PART_OF {branch_level: 1, status: "active"}]->(:Root)
239
247
  WHERE dest_e.from <= $at AND (dest_e.to IS NULL OR dest_e.to > $at)
240
- OPTIONAL MATCH (d)-[delete_edge:IS_PART_OF {status: "deleted", branch: $destination_branch}]->(:Root)
248
+ OPTIONAL MATCH (d)-[delete_edge:IS_PART_OF {status: "deleted", branch_level: 1}]->(:Root)
241
249
  WHERE delete_edge.from <= $at
242
250
  WITH *, d WHERE delete_edge IS NULL
243
251
  """
244
- self.params["destination_branch"] = destination_branch.name
245
252
  else:
246
253
  destination_filter, destination_filter_params = destination_branch.get_query_filter_path(
247
254
  at=self.at, variable_name="r", params_prefix="dst_"
@@ -288,7 +295,7 @@ class RelationshipCreateQuery(RelationshipQuery):
288
295
  self.params["at"] = self.at.to_string()
289
296
 
290
297
  self.params["is_protected"] = self.rel.is_protected
291
- self.params["is_visible"] = self.rel.is_visible
298
+ self.params["user_id"] = self.user_id
292
299
 
293
300
  self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
294
301
  self.add_dest_match_to_query(
@@ -297,35 +304,72 @@ class RelationshipCreateQuery(RelationshipQuery):
297
304
  )
298
305
  self.query_add_all_node_property_match()
299
306
 
300
- self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.ACTIVE)
307
+ self.params["rel_prop"] = self.get_relationship_properties_dict(
308
+ status=RelationshipStatus.ACTIVE, user_id=self.user_id
309
+ )
301
310
  arrows = self.schema.get_query_arrows()
302
- r1 = f"{arrows.left.start}[r1:{self.rel_type} $rel_prop ]{arrows.left.end}"
303
- r2 = f"{arrows.right.start}[r2:{self.rel_type} $rel_prop ]{arrows.right.end}"
311
+ r1 = f"{arrows.left.start}[r1:IS_RELATED $rel_prop ]{arrows.left.end}"
312
+ r2 = f"{arrows.right.start}[r2:IS_RELATED $rel_prop ]{arrows.right.end}"
313
+
314
+ relationship_create_query = (
315
+ "CREATE (rl:Relationship { uuid: $uuid, name: $name, branch_support: $branch_support"
316
+ )
317
+ if self.branch.is_default or self.branch.is_global:
318
+ relationship_create_query += (
319
+ ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
320
+ )
321
+ relationship_create_query += " })"
322
+ self.add_to_query(relationship_create_query)
304
323
 
305
324
  query_create = """
306
- CREATE (rl:Relationship { uuid: $uuid, name: $name, branch_support: $branch_support })
307
325
  CREATE (s)%s(rl)
308
326
  CREATE (rl)%s(d)
309
327
  MERGE (ip:Boolean { value: $is_protected })
310
- MERGE (iv:Boolean { value: $is_visible })
311
328
  CREATE (rl)-[r3:IS_PROTECTED $rel_prop ]->(ip)
312
- CREATE (rl)-[r4:IS_VISIBLE $rel_prop ]->(iv)
313
329
  """ % (
314
330
  r1,
315
331
  r2,
316
332
  )
333
+ if self.branch.is_default or self.branch.is_global:
334
+ query_create += """
335
+ SET s.updated_at = $at, s.updated_by = $user_id
336
+ SET d.updated_at = $at, d.updated_by = $user_id
337
+ """
317
338
 
318
339
  self.add_to_query(query_create)
319
- self.return_labels = ["s", "d", "rl", "r1", "r2", "r3", "r4"]
340
+ self.return_labels = ["s", "d", "rl", "r1", "r2", "r3"]
320
341
  self.query_add_all_node_property_create()
321
342
 
322
343
  def query_add_all_node_property_match(self) -> None:
344
+ if not self.rel._node_properties:
345
+ return
346
+
347
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
348
+ self.params.update(branch_params)
349
+
323
350
  for prop_name in self.rel._node_properties:
324
351
  if hasattr(self.rel, f"{prop_name}_id") and getattr(self.rel, f"{prop_name}_id"):
325
- self.query_add_node_property_match(name=prop_name)
326
-
327
- def query_add_node_property_match(self, name: str) -> None:
328
- self.add_to_query("MATCH (%s { uuid: $prop_%s_id })" % (name, name))
352
+ self.query_add_node_property_match(name=prop_name, branch_filter=branch_filter)
353
+
354
+ def query_add_node_property_match(self, name: str, branch_filter: str) -> None:
355
+ if self.branch.is_default or self.branch.is_global:
356
+ match_property_peer_query = """
357
+ MATCH (%(var_name)s:Node { uuid: $prop_%(var_name)s_id })
358
+ WHERE NOT exists((%(var_name)s)-[:IS_PART_OF {branch: $branch, status: "deleted"}]->(:Root))
359
+ """ % {"var_name": name}
360
+ else:
361
+ match_property_peer_query = """
362
+ CALL () {
363
+ MATCH (%(var_name)s:Node { uuid: $prop_%(var_name)s_id })-[r:IS_PART_OF]->(:Root)
364
+ WHERE %(branch_filter)s
365
+ RETURN %(var_name)s, r.status = "active" AS %(var_name)s_is_active
366
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
367
+ LIMIT 1
368
+ }
369
+ WITH *
370
+ WHERE %(var_name)s_is_active = TRUE
371
+ """ % {"var_name": name, "branch_filter": branch_filter}
372
+ self.add_to_query(match_property_peer_query)
329
373
  self.params[f"prop_{name}_id"] = getattr(self.rel, f"{name}_id")
330
374
  self.return_labels.append(name)
331
375
 
@@ -336,7 +380,7 @@ class RelationshipCreateQuery(RelationshipQuery):
336
380
 
337
381
  def query_add_node_property_create(self, name: str) -> None:
338
382
  query = """
339
- CREATE (rl)-[:HAS_%s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(%s)
383
+ CREATE (rl)-[:HAS_%s { branch: $branch, branch_level: $branch_level, status: "active", from: $at, from_user_id: $user_id }]->(%s)
340
384
  """ % (
341
385
  name.upper(),
342
386
  name,
@@ -347,15 +391,15 @@ class RelationshipCreateQuery(RelationshipQuery):
347
391
  class RelationshipUpdatePropertyQuery(RelationshipQuery):
348
392
  name = "relationship_property_update"
349
393
  type = QueryType.WRITE
394
+ insert_return = False
395
+ raise_error_if_empty = False
350
396
 
351
397
  def __init__(
352
398
  self,
353
- rel_node_id: str,
354
399
  flag_properties_to_update: dict[str, bool],
355
400
  node_properties_to_update: dict[str, str],
356
401
  **kwargs,
357
402
  ):
358
- self.rel_node_id = rel_node_id
359
403
  if not flag_properties_to_update and not node_properties_to_update:
360
404
  raise ValueError("Either flag_properties_to_update or node_properties_to_update must be set")
361
405
  self.flag_properties_to_update = flag_properties_to_update
@@ -363,22 +407,75 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
363
407
  super().__init__(**kwargs)
364
408
 
365
409
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
366
- self.params["rel_node_id"] = self.rel_node_id
410
+ self.params["rel_node_id"] = self.rel_id or (self.rel.id if self.rel else None)
367
411
  self.params["branch"] = self.branch.name
368
412
  self.params["branch_level"] = self.branch.hierarchy_level
413
+ self.params["user_id"] = self.user_id
369
414
  self.params["at"] = self.at.to_string()
370
415
 
371
- query = """
416
+ rel_query = """
372
417
  MATCH (rl:Relationship { uuid: $rel_node_id })
373
418
  """
374
- self.add_to_query(query)
419
+ if self.branch.is_default or self.branch.is_global:
420
+ rel_query += """
421
+ SET rl.updated_at = $at, rl.updated_by = $user_id
422
+ WITH rl
423
+ """
424
+ self.add_to_query(rel_query)
425
+
426
+ self.params["property_types_to_update"] = sorted(
427
+ [flag.upper() for flag in self.flag_properties_to_update.keys()]
428
+ )
429
+ self.params["property_types_to_update"] += sorted(
430
+ [f"HAS_{prop.upper()}" for prop in self.node_properties_to_update.keys()]
431
+ )
432
+ set_to_time_on_current_property_query = """
433
+ OPTIONAL MATCH (rl)-[r]->()
434
+ WHERE type(r) IN $property_types_to_update
435
+ AND r.branch = $branch
436
+ AND r.status = "active"
437
+ AND r.to IS NULL
438
+ SET r.to = $at, r.to_user_id = $user_id
439
+ WITH rl
440
+ """
441
+ self.add_to_query(set_to_time_on_current_property_query)
375
442
 
376
- self.query_add_all_node_property_merge()
443
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
444
+ self.params.update(branch_params)
445
+
446
+ self.query_add_all_node_property_merge(branch_filter=branch_filter)
377
447
  self.query_add_all_flag_property_merge()
378
448
 
379
- self.query_add_all_node_property_create()
449
+ self.query_add_all_node_property_create(branch_filter=branch_filter)
380
450
  self.query_add_all_flag_property_create()
381
451
 
452
+ # Update peer node metadata at the end (only on default/global branch)
453
+ if self.branch.is_default or self.branch.is_global:
454
+ peer_metadata_query = """
455
+ WITH rl
456
+ CALL (rl) {
457
+ MATCH (peer:Node)-[r_rel:IS_RELATED]-(rl)
458
+ WHERE r_rel.branch_level = 1
459
+ WITH DISTINCT peer, rl
460
+ CALL (peer, rl) {
461
+ MATCH (peer)-[r_rel:IS_RELATED]-(rl)
462
+ WHERE r_rel.branch_level = 1
463
+ ORDER BY r_rel.from DESC, r_rel.status ASC
464
+ LIMIT 1
465
+ WITH peer, r_rel
466
+ WHERE r_rel.status = "active" AND r_rel.to IS NULL
467
+ MATCH (peer)-[r_part:IS_PART_OF]->(:Root)
468
+ WHERE r_part.branch_level = 1
469
+ ORDER BY r_part.from DESC, r_part.status ASC
470
+ LIMIT 1
471
+ WITH peer, r_part
472
+ WHERE r_part.status = "active" AND r_part.to IS NULL
473
+ SET peer.updated_at = $at, peer.updated_by = $user_id
474
+ }
475
+ }
476
+ """
477
+ self.add_to_query(peer_metadata_query)
478
+
382
479
  def query_add_all_flag_property_merge(self) -> None:
383
480
  for prop_name, prop_value in self.flag_properties_to_update.items():
384
481
  self.query_add_flag_property_merge(name=prop_name, value=prop_value)
@@ -386,34 +483,30 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
386
483
  def query_add_flag_property_merge(self, name: str, value: bool) -> None:
387
484
  self.add_to_query("MERGE (prop_%s:Boolean { value: $prop_%s })" % (name, name))
388
485
  self.params[f"prop_{name}"] = value
389
- self.return_labels.append(f"prop_{name}")
390
-
391
- def query_add_all_node_property_merge(self) -> None:
392
- branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
393
- self.params.update(branch_params)
394
486
 
487
+ def query_add_all_node_property_merge(self, branch_filter: str) -> None:
395
488
  for prop_name, prop_value in self.node_properties_to_update.items():
396
489
  self.params[f"prop_{prop_name}"] = prop_value
397
490
  if self.branch.is_default or self.branch.is_global:
398
491
  node_query = """
399
- MATCH (prop_%(prop_name)s:Node {uuid: $prop_%(prop_name)s })-[r_%(prop_name)s:IS_PART_OF]->(:Root)
492
+ OPTIONAL MATCH (prop_%(prop_name)s:Node {uuid: $prop_%(prop_name)s })-[r_%(prop_name)s:IS_PART_OF]->(:Root)
400
493
  WHERE r_%(prop_name)s.branch IN $branch0
401
494
  AND r_%(prop_name)s.status = "active"
402
495
  AND r_%(prop_name)s.from <= $at AND (r_%(prop_name)s.to IS NULL OR r_%(prop_name)s.to > $at)
403
496
  WITH *
497
+ WHERE $prop_%(prop_name)s IS NULL OR prop_%(prop_name)s IS NOT NULL
404
498
  LIMIT 1
405
499
  """ % {"prop_name": prop_name}
406
500
  else:
407
501
  node_query = """
408
- MATCH (prop_%(prop_name)s:Node {uuid: $prop_%(prop_name)s })-[r_%(prop_name)s:IS_PART_OF]->(:Root)
502
+ OPTIONAL MATCH (prop_%(prop_name)s:Node {uuid: $prop_%(prop_name)s })-[r_%(prop_name)s:IS_PART_OF]->(:Root)
409
503
  WHERE all(r in [r_%(prop_name)s] WHERE %(branch_filter)s)
410
504
  ORDER BY r_%(prop_name)s.branch_level DESC, r_%(prop_name)s.from DESC, r_%(prop_name)s.status ASC
411
505
  LIMIT 1
412
506
  WITH *
413
- WHERE r_%(prop_name)s.status = "active"
507
+ WHERE $prop_%(prop_name)s IS NULL OR r_%(prop_name)s.status = "active"
414
508
  """ % {"branch_filter": branch_filter, "prop_name": prop_name}
415
509
  self.add_to_query(node_query)
416
- self.return_labels.append(f"prop_{prop_name}")
417
510
 
418
511
  def query_add_all_flag_property_create(self) -> None:
419
512
  for prop_name in self.flag_properties_to_update:
@@ -421,172 +514,158 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
421
514
 
422
515
  def query_add_flag_property_create(self, name: str) -> None:
423
516
  query = """
424
- CREATE (rl)-[:%s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(prop_%s)
517
+ CREATE (rl)-[:%s { branch: $branch, branch_level: $branch_level, status: "active", from: $at, from_user_id: $user_id }]->(prop_%s)
425
518
  """ % (
426
519
  name.upper(),
427
520
  name,
428
521
  )
429
522
  self.add_to_query(query)
430
523
 
431
- def query_add_all_node_property_create(self) -> None:
524
+ def query_add_all_node_property_create(self, branch_filter: str) -> None:
432
525
  for prop_name in self.node_properties_to_update:
433
- self.query_add_node_property_create(name=prop_name)
434
-
435
- def query_add_node_property_create(self, name: str) -> None:
436
- query = """
437
- CREATE (rl)-[:%s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(prop_%s)
438
- """ % (
439
- "HAS_" + name.upper(),
440
- name,
441
- )
442
- self.add_to_query(query)
443
-
444
-
445
- class RelationshipDataDeleteQuery(RelationshipQuery):
446
- name = "relationship_data_delete"
447
- type = QueryType.WRITE
448
-
449
- def __init__(
450
- self,
451
- data: RelationshipPeerData,
452
- **kwargs,
453
- ):
454
- self.data = data
455
- super().__init__(**kwargs)
526
+ self.query_add_node_property_create(name=prop_name, branch_filter=branch_filter)
456
527
 
457
- async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
458
- self.params["source_id"] = self.source_id
459
- self.params["rel_node_id"] = self.data.rel_node_id
460
- self.params["name"] = self.schema.identifier
461
- self.params["branch"] = self.branch.name
462
- self.params["branch_level"] = self.branch.hierarchy_level
463
- self.params["at"] = self.at.to_string()
464
-
465
- # -----------------------------------------------------------------------
466
- # Match all nodes, including properties
467
- # -----------------------------------------------------------------------
468
-
469
- self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
470
- self.add_dest_match_to_query(destination_branch=self.branch, destination_id=self.data.peer_id)
528
+ def query_add_node_property_create(self, name: str, branch_filter: str) -> None:
471
529
  query = """
472
- MATCH (rl:Relationship { uuid: $rel_node_id })
473
- """
474
- self.add_to_query(query)
475
- self.return_labels = ["s", "d", "rl"]
476
-
477
- for prop_name, prop in self.data.properties.items():
478
- self.add_to_query(
479
- "MATCH (prop_%(prop_name)s) WHERE %(id_func)s(prop_%(prop_name)s) = $prop_%(prop_name)s_id"
480
- % {"prop_name": prop_name, "id_func": db.get_id_function_name()}
481
- )
482
- self.params[f"prop_{prop_name}_id"] = db.to_database_id(prop.prop_db_id)
483
- self.return_labels.append(f"prop_{prop_name}")
484
-
485
- self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.DELETED)
486
-
487
- arrows = self.schema.get_query_arrows()
488
- r1 = f"{arrows.left.start}[r1:{self.rel_type} $rel_prop ]{arrows.left.end}"
489
- r2 = f"{arrows.right.start}[r2:{self.rel_type} $rel_prop ]{arrows.right.end}"
490
-
491
- # -----------------------------------------------------------------------
492
- # Create all the DELETE relationships, including properties
493
- # -----------------------------------------------------------------------
494
- query = """
495
- CREATE (s)%s(rl)
496
- CREATE (rl)%s(d)
497
- """ % (
498
- r1,
499
- r2,
500
- )
530
+ WITH *
531
+ CALL (rl, prop_%(var_name)s) {
532
+ WITH rl, prop_%(var_name)s
533
+ WHERE $prop_%(var_name)s IS NOT NULL
534
+ CREATE (rl)
535
+ -[:%(edge_type)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at, from_user_id: $user_id }]
536
+ ->(prop_%(var_name)s)
537
+ }
538
+ CALL (rl) {
539
+ WITH rl
540
+ WHERE $prop_%(var_name)s IS NULL
541
+ AND $branch_level > 1
542
+ MATCH (rl)-[r:%(edge_type)s]->(current_prop)
543
+ WHERE %(branch_filter)s
544
+ AND r.branch_level = 1
545
+ AND r.status = "active"
546
+ CREATE (rl)
547
+ -[:%(edge_type)s { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]
548
+ ->(current_prop)
549
+ }
550
+ """ % {"edge_type": "HAS_" + name.upper(), "var_name": name, "branch_filter": branch_filter}
501
551
  self.add_to_query(query)
502
- self.return_labels.extend(["r1", "r2"])
503
-
504
- for prop_name, prop in self.data.properties.items():
505
- self.add_to_query(
506
- "CREATE (prop_%s)<-[rel_prop_%s:%s $rel_prop ]-(rl)" % (prop_name, prop_name, prop.rel.type),
507
- )
508
- self.return_labels.append(f"rel_prop_{prop_name}")
509
552
 
510
553
 
511
554
  class RelationshipDeleteQuery(RelationshipQuery):
512
555
  name = "relationship_delete"
513
556
  type = QueryType.WRITE
557
+ insert_return = False
558
+ raise_error_if_empty = False
514
559
 
515
- def __init__(self, **kwargs):
560
+ def __init__(self, source_branch: Branch, destination_branch: Branch, **kwargs):
561
+ self.source_branch = source_branch
562
+ self.destination_branch = destination_branch
516
563
  super().__init__(**kwargs)
517
564
 
518
- if inspect.isclass(self.rel):
519
- raise TypeError("An instance of Relationship must be provided to RelationshipDeleteQuery")
565
+ if inspect.isclass(self.rel) and not self.rel_id:
566
+ raise TypeError(
567
+ "An instance of Relationship or a relationship ID must be provided to RelationshipDeleteQuery"
568
+ )
520
569
 
521
570
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
522
571
  rel_filter, rel_params = self.branch.get_query_filter_path(at=self.at, variable_name="edge")
523
- self.params["rel_id"] = self.rel.id
572
+ self.params["rel_id"] = self.rel_id or self.rel.id
524
573
  self.params["branch"] = self.branch.name
525
- self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.DELETED)
574
+ self.params["rel_prop"] = self.get_relationship_properties_dict(
575
+ status=RelationshipStatus.DELETED, user_id=self.user_id
576
+ )
577
+ self.params["user_id"] = self.user_id
526
578
  self.params["at"] = self.at.to_string()
527
579
  self.params.update(rel_params)
528
580
 
529
581
  arrows = self.schema.get_query_arrows()
530
- r1 = f"{arrows.left.start}[r1:{self.rel_type} $rel_prop ]{arrows.left.end}"
531
- r2 = f"{arrows.right.start}[r2:{self.rel_type} $rel_prop ]{arrows.right.end}"
582
+ r1 = f"{arrows.left.start}[r1:IS_RELATED $rel_prop ]{arrows.left.end}"
583
+ r2 = f"{arrows.right.start}[r2:IS_RELATED $rel_prop ]{arrows.right.end}"
532
584
 
533
- self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
585
+ self.add_source_match_to_query(source_branch=self.source_branch)
534
586
  self.add_dest_match_to_query(
535
- destination_branch=self.destination.get_branch_based_on_support_type(),
587
+ destination_branch=self.destination_branch,
536
588
  destination_id=self.destination_id or self.destination.get_id(),
537
589
  )
538
- query = """
539
- MATCH (s)-[:IS_RELATED]-(rl:Relationship {uuid: $rel_id})-[:IS_RELATED]-(d)
540
- WITH DISTINCT s, rl, d
590
+
591
+ # if the IS_RELATED edges are already deleted on this branch, then we assume the delete already succeeded
592
+ rel_match_query = """
593
+ MATCH (s)%(arrows_l1)s[r1:IS_RELATED]%(arrows_l2)s(rl:Relationship { uuid: $rel_id })%(arrows_r1)s[r2:IS_RELATED]%(arrows_r2)s(d)
594
+ WHERE all(edge in [r1, r2] WHERE %(rel_filter)s)
595
+ ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
541
596
  LIMIT 1
542
- CREATE (s)%(r1)s(rl)
543
- CREATE (rl)%(r2)s(d)
544
- WITH rl
545
- CALL (rl) {
546
- MATCH (rl)-[edge:IS_VISIBLE]->(visible)
547
- WHERE %(rel_filter)s AND edge.status = "active"
548
- WITH rl, edge, visible
549
- ORDER BY edge.branch_level DESC
550
- LIMIT 1
551
- CREATE (rl)-[deleted_edge:IS_VISIBLE $rel_prop]->(visible)
552
- WITH edge
553
- WHERE edge.branch = $branch
554
- SET edge.to = $at
597
+ WITH s, rl, d, r1 AS source_edge, r2 AS destination_edge
598
+ WHERE source_edge.status = "active" OR destination_edge.status = "active"
599
+ """ % {
600
+ "rel_filter": rel_filter,
601
+ "arrows_l1": arrows.left.start,
602
+ "arrows_l2": arrows.left.end,
603
+ "arrows_r1": arrows.right.start,
604
+ "arrows_r2": arrows.right.end,
555
605
  }
556
- CALL (rl) {
557
- MATCH (rl)-[edge:IS_PROTECTED]->(protected)
558
- WHERE %(rel_filter)s AND edge.status = "active"
559
- WITH rl, edge, protected
560
- ORDER BY edge.branch_level DESC
561
- LIMIT 1
562
- CREATE (rl)-[deleted_edge:IS_PROTECTED $rel_prop]->(protected)
563
- WITH edge
564
- WHERE edge.branch = $branch
565
- SET edge.to = $at
606
+ if self.branch.is_default or self.branch.is_global:
607
+ rel_match_query += """
608
+ SET rl.updated_at = $at, rl.updated_by = $user_id
609
+ SET s.updated_at = $at, s.updated_by = $user_id
610
+ SET d.updated_at = $at, d.updated_by = $user_id
611
+ """
612
+ self.add_to_query(rel_match_query)
613
+
614
+ query = """
615
+ WITH s, rl, d, source_edge, destination_edge
616
+ // --------------
617
+ // create the deleted edges if the existing edges are on global or default branch
618
+ // --------------
619
+ CALL (s, source_edge, rl) {
620
+ WITH source_edge
621
+ WHERE source_edge.branch <> $branch
622
+ CREATE (s)%(r1)s(rl)
566
623
  }
567
- CALL (rl) {
568
- MATCH (rl)-[edge:HAS_OWNER]->(owner_node)
569
- WHERE %(rel_filter)s AND edge.status = "active"
570
- WITH rl, edge, owner_node
571
- ORDER BY edge.branch_level DESC
572
- LIMIT 1
573
- CREATE (rl)-[deleted_edge:HAS_OWNER $rel_prop]->(owner_node)
624
+ CALL (rl, destination_edge, d) {
625
+ WITH destination_edge
626
+ WHERE destination_edge.branch <> $branch
627
+ CREATE (rl)%(r2)s(d)
628
+ }
629
+ // --------------
630
+ // set the to time on the existing edges if they are on the current branch
631
+ // --------------
632
+ CALL (s, source_edge, rl) {
633
+ WITH source_edge
634
+ WHERE source_edge.branch = $branch
635
+ AND source_edge.to IS NULL
636
+ SET source_edge.to = $at, source_edge.to_user_id = $user_id
637
+ }
638
+ CALL (rl, destination_edge, d) {
639
+ WITH destination_edge
640
+ WHERE destination_edge.branch = $branch
641
+ AND destination_edge.to IS NULL
642
+ SET destination_edge.to = $at, destination_edge.to_user_id = $user_id
643
+ }
644
+ WITH rl
645
+
646
+ OPTIONAL MATCH (rl)-[edge:IS_PROTECTED|HAS_OWNER|HAS_SOURCE]->(peer)
647
+ WHERE %(rel_filter)s
648
+ ORDER BY type(edge), edge.branch_level DESC, edge.from DESC, edge.status ASC
649
+ WITH rl, type(edge) AS edge_type, head(collect(edge)) AS edge, head(collect(peer)) AS peer
650
+
651
+ CALL (rl, edge, edge_type, peer) {
574
652
  WITH edge
575
- WHERE edge.branch = $branch
576
- SET edge.to = $at
653
+ WHERE edge.branch <> $branch
654
+ AND edge.status = "active"
655
+ CREATE (rl)-[deleted_edge:$(edge_type) $rel_prop]->(peer)
577
656
  }
578
- CALL (rl) {
579
- MATCH (rl)-[edge:HAS_SOURCE]->(source_node)
580
- WHERE %(rel_filter)s AND edge.status = "active"
581
- WITH rl, edge, source_node
582
- ORDER BY edge.branch_level DESC
583
- LIMIT 1
584
- CREATE (rl)-[deleted_edge:HAS_SOURCE $rel_prop]->(source_node)
657
+ CALL (edge) {
585
658
  WITH edge
586
659
  WHERE edge.branch = $branch
587
- SET edge.to = $at
660
+ AND edge.status = "active"
661
+ AND edge.to IS NULL
662
+ SET edge.to = $at, edge.to_user_id = $user_id
663
+ }
664
+ """ % {
665
+ "r1": r1,
666
+ "r2": r2,
667
+ "rel_filter": rel_filter,
588
668
  }
589
- """ % {"r1": r1, "r2": r2, "rel_filter": rel_filter}
590
669
 
591
670
  self.params["at"] = self.at.to_string()
592
671
  self.return_labels = ["rl"]
@@ -609,6 +688,7 @@ class RelationshipGetPeerQuery(Query):
609
688
  schema: RelationshipSchema | None = None,
610
689
  branch: Branch | None = None,
611
690
  at: Timestamp | str | None = None,
691
+ include_metadata: MetadataOptions = MetadataOptions.NONE,
612
692
  **kwargs,
613
693
  ):
614
694
  if not source and not source_ids:
@@ -631,6 +711,7 @@ class RelationshipGetPeerQuery(Query):
631
711
  self.rel = rel
632
712
  self.rel_type = rel_type or self.rel.rel_type
633
713
  self.schema = schema or self.rel.schema
714
+ self.include_metadata = include_metadata
634
715
 
635
716
  if not branch and inspect.isclass(rel) and not hasattr(rel, "branch"):
636
717
  raise ValueError("Either an instance of Relationship or a valid branch must be provided.")
@@ -644,6 +725,124 @@ class RelationshipGetPeerQuery(Query):
644
725
 
645
726
  super().__init__(**kwargs)
646
727
 
728
+ def _add_is_protected_query(self, branch_filter: str) -> None:
729
+ if not (self.include_metadata & MetadataOptions.IS_PROTECTED):
730
+ return
731
+ query = """
732
+ CALL (rl) {
733
+ MATCH (rl)-[r:IS_PROTECTED]-(is_protected)
734
+ WHERE %(branch_filter)s
735
+ RETURN r AS rel_is_protected, is_protected
736
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
737
+ LIMIT 1
738
+ }
739
+ """ % {"branch_filter": branch_filter}
740
+ self.add_to_query(query)
741
+ self.update_return_labels(["rel_is_protected", "is_protected"])
742
+
743
+ def _add_node_property_query(self, node_prop: str, branch_filter: str) -> None:
744
+ query = """
745
+ CALL (rl) {
746
+ OPTIONAL MATCH (rl)-[r:HAS_%(node_prop_type)s]-(%(node_prop)s)
747
+ WHERE %(branch_filter)s
748
+ WITH r, %(node_prop)s
749
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
750
+ LIMIT 1
751
+ RETURN CASE
752
+ WHEN r.status = "active" THEN %(node_prop)s
753
+ ELSE NULL
754
+ END AS %(node_prop)s,
755
+ CASE
756
+ WHEN r.status = "active" THEN r
757
+ ELSE NULL
758
+ END AS rel_%(node_prop)s
759
+ }
760
+ """ % {
761
+ "node_prop": node_prop,
762
+ "node_prop_type": node_prop.upper(),
763
+ "branch_filter": branch_filter,
764
+ }
765
+ self.add_to_query(query)
766
+ self.update_return_labels([f"rel_{node_prop}", node_prop])
767
+
768
+ def _add_has_owner_query(self, branch_filter: str) -> None:
769
+ if not (self.include_metadata & MetadataOptions.OWNER):
770
+ return
771
+ self._add_node_property_query(node_prop="owner", branch_filter=branch_filter)
772
+
773
+ def _add_has_source_query(self, branch_filter: str) -> None:
774
+ if not (self.include_metadata & MetadataOptions.SOURCE):
775
+ return
776
+ self._add_node_property_query(node_prop="source", branch_filter=branch_filter)
777
+
778
+ def _add_created_metadata_to_query(self) -> None:
779
+ if not (self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY)):
780
+ return
781
+ if self.branch.is_default or self.branch.is_global:
782
+ last_created_query = """
783
+ WITH *, rl.created_at AS created_at, rl.created_by AS created_by
784
+ """
785
+ else:
786
+ last_created_query = """
787
+ CALL (rels) {
788
+ UNWIND rels AS rel
789
+ RETURN rel.from AS created_at, rel.from_user_id AS created_by
790
+ ORDER BY created_at ASC
791
+ LIMIT 1
792
+ }
793
+ """
794
+ self.add_to_query(last_created_query)
795
+ self.update_return_labels(["created_at", "created_by"])
796
+
797
+ def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
798
+ if not (self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY)):
799
+ return
800
+ self.update_return_labels(["updated_at", "updated_by"])
801
+ if self.branch.is_default or self.branch.is_global:
802
+ last_updated_query = """
803
+ WITH *, rl.updated_at AS updated_at, rl.updated_by AS updated_by
804
+ """
805
+ self.add_to_query(last_updated_query)
806
+ return
807
+
808
+ # query for non-default, non-global branches
809
+ if self.branch_agnostic:
810
+ time_details = """
811
+ WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
812
+ """
813
+ else:
814
+ time_details = """
815
+ WITH CASE
816
+ WHEN $is_branch_agnostic THEN [r.from, r.from_user_id]
817
+ WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
818
+ WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
819
+ ELSE [NULL, NULL]
820
+ END AS from_details,
821
+ CASE
822
+ WHEN $is_branch_agnostic THEN [r.to, r.to_user_id]
823
+ WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
824
+ WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
825
+ ELSE [NULL, NULL]
826
+ END AS to_details
827
+ """
828
+ last_updated_query = """
829
+ CALL (rl) {
830
+ MATCH (rl)-[r]-(property)
831
+ WHERE %(branch_filter)s
832
+ %(time_details)s
833
+ WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
834
+ WITH from_details_list + to_details_list AS details_list
835
+ UNWIND details_list AS one_details
836
+ WITH one_details[0] AS updated_at, one_details[1] AS updated_by
837
+ WHERE updated_at IS NOT NULL
838
+ WITH updated_at, updated_by
839
+ ORDER BY updated_at DESC
840
+ LIMIT 1
841
+ RETURN updated_at, updated_by
842
+ }
843
+ """ % {"branch_filter": branch_filter_str, "time_details": time_details}
844
+ self.add_to_query(last_updated_query)
845
+
647
846
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
648
847
  branch_filter, branch_params = self.branch.get_query_filter_path(
649
848
  at=self.at, branch_agnostic=self.branch_agnostic
@@ -653,6 +852,7 @@ class RelationshipGetPeerQuery(Query):
653
852
 
654
853
  peer_schema = self.schema.get_peer_schema(db=db, branch=self.branch)
655
854
 
855
+ self.params["is_branch_agnostic"] = self.branch_agnostic
656
856
  self.params["source_ids"] = self.source_ids
657
857
  self.params["rel_identifier"] = self.schema.identifier
658
858
  self.params["peer_kind"] = self.schema.peer
@@ -738,47 +938,13 @@ class RelationshipGetPeerQuery(Query):
738
938
  )
739
939
  self.add_subquery(subquery=subquery, node_alias="peer", with_clause=with_str)
740
940
  # ----------------------------------------------------------------------------
741
- # QUERY Properties
941
+ # add metadata
742
942
  # ----------------------------------------------------------------------------
743
- query = """
744
- CALL (rl) {
745
- MATCH (rl)-[r:IS_VISIBLE]-(is_visible)
746
- WHERE %(branch_filter)s
747
- RETURN r AS rel_is_visible, is_visible
748
- ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
749
- LIMIT 1
750
- }
751
- CALL (rl) {
752
- MATCH (rl)-[r:IS_PROTECTED]-(is_protected)
753
- WHERE %(branch_filter)s
754
- RETURN r AS rel_is_protected, is_protected
755
- ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
756
- LIMIT 1
757
- }
758
- """ % {"branch_filter": branch_filter}
759
-
760
- self.add_to_query(query)
761
-
762
- self.update_return_labels(["rel_is_visible", "rel_is_protected", "is_visible", "is_protected"])
763
-
764
- # Add Node Properties
765
- # We must query them one by one otherwise the second one won't return
766
- for node_prop in ["source", "owner"]:
767
- query = """
768
- CALL (rl) {
769
- OPTIONAL MATCH (rl)-[r:HAS_%(node_prop_type)s]-(%(node_prop)s)
770
- WHERE %(branch_filter)s
771
- RETURN r AS rel_%(node_prop)s, %(node_prop)s
772
- ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
773
- LIMIT 1
774
- }
775
- """ % {
776
- "node_prop": node_prop,
777
- "node_prop_type": node_prop.upper(),
778
- "branch_filter": branch_filter,
779
- }
780
- self.add_to_query(query)
781
- self.update_return_labels([f"rel_{node_prop}", node_prop])
943
+ self._add_is_protected_query(branch_filter)
944
+ self._add_has_owner_query(branch_filter)
945
+ self._add_has_source_query(branch_filter)
946
+ self._add_created_metadata_to_query()
947
+ self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
782
948
 
783
949
  self.add_to_query("WITH " + ",".join(self.return_labels))
784
950
 
@@ -823,6 +989,17 @@ class RelationshipGetPeerQuery(Query):
823
989
  rels = result.get("rels")
824
990
  source_node = result.get_node("source_node")
825
991
  peer_node = result.get_node("peer")
992
+
993
+ if self.include_metadata & MetadataOptions.CREATED_AT:
994
+ created_at_str = result.get("created_at")
995
+ created_at = Timestamp(created_at_str) if created_at_str else None
996
+ else:
997
+ created_at = None
998
+ if self.include_metadata & MetadataOptions.UPDATED_AT:
999
+ updated_at_str = result.get("updated_at")
1000
+ updated_at = Timestamp(updated_at_str) if updated_at_str else None
1001
+ else:
1002
+ updated_at = None
826
1003
  data = RelationshipPeerData(
827
1004
  source_id=source_node.get("uuid"),
828
1005
  source_db_id=source_node.element_id,
@@ -832,88 +1009,44 @@ class RelationshipGetPeerQuery(Query):
832
1009
  peer_kind=peer_node.get("kind"),
833
1010
  rel_node_db_id=result.get("rl").element_id,
834
1011
  rel_node_id=result.get("rl").get("uuid"),
835
- updated_at=rels[0]["from"],
836
1012
  rels=[RelData.from_db(rel) for rel in rels],
837
1013
  branch=self.branch.name,
1014
+ created_at=created_at,
1015
+ created_by=result.get("created_by") if self.include_metadata & MetadataOptions.CREATED_BY else None,
1016
+ updated_at=updated_at,
1017
+ updated_by=result.get("updated_by") if self.include_metadata & MetadataOptions.UPDATED_BY else None,
838
1018
  properties={},
839
1019
  )
840
1020
 
841
- if hasattr(self.rel, "_flag_properties"):
842
- for prop in self.rel._flag_properties:
843
- if prop_node := result.get(prop):
844
- data.properties[prop] = FlagPropertyData(
845
- name=prop,
846
- prop_db_id=prop_node.element_id,
847
- rel=RelData.from_db(result.get(f"rel_{prop}")),
848
- value=prop_node.get("value"),
849
- )
850
-
851
- if hasattr(self.rel, "_node_properties"):
852
- for prop in self.rel._node_properties:
853
- if prop_node := result.get(prop):
854
- data.properties[prop] = NodePropertyData(
855
- name=prop,
856
- prop_db_id=prop_node.element_id,
857
- rel=RelData.from_db(result.get(f"rel_{prop}")),
858
- value=prop_node.get("uuid"),
859
- )
860
-
861
- yield data
862
-
863
-
864
- class RelationshipGetQuery(RelationshipQuery):
865
- name = "relationship_get"
866
-
867
- type: QueryType = QueryType.READ
868
-
869
- async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
870
- self.params["name"] = self.schema.identifier
871
- self.params["branch"] = self.branch.name
872
-
873
- rels_filter, rels_params = self.branch.get_query_filter_relationships(
874
- rel_labels=["r1", "r2"], at=self.at.to_string(), include_outside_parentheses=True
875
- )
876
-
877
- self.params.update(rels_params)
878
-
879
- arrows = self.schema.get_query_arrows()
880
- r1 = f"{arrows.left.start}[r1:{self.rel.rel_type}]{arrows.left.end}"
881
- r2 = f"{arrows.right.start}[r2:{self.rel.rel_type}]{arrows.right.end}"
882
-
883
- self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type())
884
- self.add_dest_match_to_query(
885
- destination_branch=self.destination.get_branch_based_on_support_type(),
886
- destination_id=self.destination_id or self.destination.get_id(),
887
- )
888
- query = """
889
- MATCH (s)%s(rl:Relationship { name: $name })%s(d)
890
- WHERE %s
891
- ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
892
- WITH *, r1.status = "active" AND r2.status = "active" AS is_active
893
- LIMIT 1
894
- """ % (
895
- r1,
896
- r2,
897
- "\n AND ".join(rels_filter),
898
- )
899
-
900
- self.params["at"] = self.at.to_string()
901
-
902
- self.add_to_query(query)
903
- self.return_labels = ["s", "d", "rl", "r2", "is_active"]
904
-
905
- def is_already_deleted(self) -> bool:
906
- result = self.get_result()
907
- if not result:
908
- return False
909
- return result.get("is_active") is False
1021
+ prop, metadata_option = ("is_protected", MetadataOptions.IS_PROTECTED)
1022
+ if self.include_metadata & metadata_option:
1023
+ prop_node = result.get(prop)
1024
+ if prop_node:
1025
+ data.properties[prop] = FlagPropertyData(
1026
+ name=prop,
1027
+ prop_db_id=prop_node.element_id,
1028
+ rel=RelData.from_db(result.get(f"rel_{prop}")),
1029
+ value=prop_node.get("value"),
1030
+ )
1031
+
1032
+ for prop, metadata_option in [("owner", MetadataOptions.OWNER), ("source", MetadataOptions.SOURCE)]:
1033
+ if not self.include_metadata & metadata_option:
1034
+ continue
1035
+ prop_node = result.get(prop)
1036
+ if not prop_node:
1037
+ continue
1038
+ data.properties[prop] = NodePropertyData(
1039
+ name=prop,
1040
+ prop_db_id=prop_node.element_id,
1041
+ rel=RelData.from_db(result.get(f"rel_{prop}")),
1042
+ value=prop_node.get("uuid"),
1043
+ )
910
1044
 
911
- def get_relationships_ids_for_branch(self, branch_name: str) -> list[str] | None:
912
- result = self.get_result()
913
- if not result:
914
- return None
1045
+ if prop == "source" and InfrahubKind.PROFILE in prop_node.labels:
1046
+ data.is_from_profile = True
1047
+ data.profile_id = prop_node._properties["uuid"]
915
1048
 
916
- return [rel.element_id for rel in result.get_rels() if rel.get("branch") == branch_name]
1049
+ yield data
917
1050
 
918
1051
 
919
1052
  class RelationshipGetByIdentifierQuery(Query):
@@ -954,7 +1087,7 @@ class RelationshipGetByIdentifierQuery(Query):
954
1087
  self.params["at"] = self.at.to_string()
955
1088
 
956
1089
  rels_filter, rels_params = self.branch.get_query_filter_relationships(
957
- rel_labels=["r1", "r2"], at=self.at.to_string(), include_outside_parentheses=True
1090
+ rel_labels=["r1", "r2"], at=self.at, include_outside_parentheses=True
958
1091
  )
959
1092
  self.params.update(rels_params)
960
1093
 
@@ -1072,12 +1205,13 @@ class RelationshipDeleteAllQuery(Query):
1072
1205
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
1073
1206
  self.params["source_id"] = kwargs["node_id"]
1074
1207
  self.params["branch"] = self.branch.name
1075
-
1208
+ self.params["user_id"] = self.user_id
1076
1209
  self.params["rel_prop"] = {
1077
1210
  "branch": self.branch.name,
1078
1211
  "branch_level": self.branch.hierarchy_level,
1079
1212
  "status": RelationshipStatus.DELETED.value,
1080
1213
  "from": self.at.to_string(),
1214
+ "from_user_id": self.user_id,
1081
1215
  }
1082
1216
 
1083
1217
  self.params["at"] = self.at.to_string()
@@ -1087,14 +1221,20 @@ class RelationshipDeleteAllQuery(Query):
1087
1221
  )
1088
1222
  self.params.update(rel_params)
1089
1223
 
1090
- query = """
1224
+ rel_match_query = """
1091
1225
  MATCH (s:Node { uuid: $source_id })-[active_edge:IS_RELATED]-(rl:Relationship)
1092
1226
  WHERE %(active_rel_filter)s AND active_edge.status = "active"
1093
- WITH DISTINCT rl
1094
1227
  """ % {"active_rel_filter": active_rel_filter}
1228
+ if self.branch.is_default or self.branch.is_global:
1229
+ rel_match_query += """
1230
+ SET rl.updated_at = $at, rl.updated_by = $user_id
1231
+ """
1232
+ rel_match_query += """
1233
+ WITH DISTINCT rl
1234
+ """
1235
+ self.add_to_query(rel_match_query)
1095
1236
 
1096
1237
  edge_types = [
1097
- DatabaseEdgeType.IS_VISIBLE.value,
1098
1238
  DatabaseEdgeType.IS_PROTECTED.value,
1099
1239
  DatabaseEdgeType.HAS_OWNER.value,
1100
1240
  DatabaseEdgeType.HAS_SOURCE.value,
@@ -1110,7 +1250,7 @@ class RelationshipDeleteAllQuery(Query):
1110
1250
  SET deleted_edge.hierarchy = active_edge.hierarchy
1111
1251
  WITH active_edge, n
1112
1252
  WHERE active_edge.branch = $branch AND active_edge.to IS NULL
1113
- SET active_edge.to = $at
1253
+ SET active_edge.to = $at, active_edge.to_user_id = $user_id
1114
1254
  }
1115
1255
  """ % {
1116
1256
  "arrow_left": arrow_left,
@@ -1119,10 +1259,14 @@ class RelationshipDeleteAllQuery(Query):
1119
1259
  "edge_type": edge_type,
1120
1260
  }
1121
1261
 
1122
- query += sub_query
1262
+ self.add_to_query(sub_query)
1123
1263
 
1124
1264
  # We only want to return uuid/kind of `Node` connected through `IS_RELATED` edges.
1125
- query += """
1265
+ peer_node_metadata_update = ""
1266
+ if self.branch.is_default or self.branch.is_global:
1267
+ peer_node_metadata_update = "SET n.updated_at = $at, n.updated_by = $user_id"
1268
+
1269
+ query = """
1126
1270
  CALL (rl) {
1127
1271
  MATCH (rl)-[active_edge:IS_RELATED]->(n)
1128
1272
  WHERE %(active_rel_filter)s
@@ -1132,9 +1276,10 @@ class RelationshipDeleteAllQuery(Query):
1132
1276
  WHERE active_edge.status = "active"
1133
1277
  CREATE (rl)-[deleted_edge:IS_RELATED $rel_prop]->(n)
1134
1278
  SET deleted_edge.hierarchy = active_edge.hierarchy
1279
+ %(peer_node_metadata_update)s
1135
1280
  WITH rl, active_edge, n
1136
1281
  WHERE active_edge.branch = $branch AND active_edge.to IS NULL
1137
- SET active_edge.to = $at
1282
+ SET active_edge.to = $at, active_edge.to_user_id = $user_id
1138
1283
  RETURN
1139
1284
  n.uuid as uuid,
1140
1285
  n.kind as kind,
@@ -1151,9 +1296,10 @@ class RelationshipDeleteAllQuery(Query):
1151
1296
  WHERE active_edge.status = "active"
1152
1297
  CREATE (rl)<-[deleted_edge:IS_RELATED $rel_prop]-(n)
1153
1298
  SET deleted_edge.hierarchy = active_edge.hierarchy
1299
+ %(peer_node_metadata_update)s
1154
1300
  WITH rl, active_edge, n
1155
1301
  WHERE active_edge.branch = $branch AND active_edge.to IS NULL
1156
- SET active_edge.to = $at
1302
+ SET active_edge.to = $at, active_edge.to_user_id = $user_id
1157
1303
  RETURN
1158
1304
  n.uuid as uuid,
1159
1305
  n.kind as kind,
@@ -1161,8 +1307,11 @@ class RelationshipDeleteAllQuery(Query):
1161
1307
  "inbound" as rel_direction
1162
1308
  }
1163
1309
  RETURN DISTINCT uuid, kind, rel_identifier, rel_direction
1164
- """ % {"active_rel_filter": active_rel_filter, "id_func": db.get_id_function_name()}
1165
-
1310
+ """ % {
1311
+ "active_rel_filter": active_rel_filter,
1312
+ "id_func": db.get_id_function_name(),
1313
+ "peer_node_metadata_update": peer_node_metadata_update,
1314
+ }
1166
1315
  self.add_to_query(query)
1167
1316
 
1168
1317
  def get_deleted_relationships_changelog(