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
@@ -8,6 +8,8 @@ from dataclasses import field as dataclass_field
8
8
  from enum import Enum
9
9
  from typing import TYPE_CHECKING, Any, AsyncIterator, Generator
10
10
 
11
+ import ujson
12
+
11
13
  from infrahub import config
12
14
  from infrahub.core import registry
13
15
  from infrahub.core.constants import (
@@ -15,6 +17,7 @@ from infrahub.core.constants import (
15
17
  PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
16
18
  PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER,
17
19
  AttributeDBNodeType,
20
+ MetadataOptions,
18
21
  RelationshipDirection,
19
22
  RelationshipHierarchyDirection,
20
23
  )
@@ -22,6 +25,7 @@ from infrahub.core.query import Query, QueryResult, QueryType
22
25
  from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
23
26
  from infrahub.core.query.utils import find_node_schema
24
27
  from infrahub.core.schema.attribute_schema import AttributeSchema
28
+ from infrahub.core.timestamp import Timestamp
25
29
  from infrahub.core.utils import build_regex_attrs, extract_field_filters
26
30
  from infrahub.exceptions import QueryError
27
31
  from infrahub.graphql.models import OrderModel
@@ -44,14 +48,15 @@ if TYPE_CHECKING:
44
48
  class NodeToProcess:
45
49
  schema: NodeSchema | ProfileSchema | TemplateSchema | None
46
50
 
51
+ labels: list[str]
47
52
  node_id: str
48
53
  node_uuid: str
49
-
50
- updated_at: str
51
-
52
54
  branch: str
53
55
 
54
- labels: list[str]
56
+ created_at: Timestamp | None = None
57
+ created_by: str | None = None
58
+ updated_at: Timestamp | None = None
59
+ updated_by: str | None = None
55
60
 
56
61
 
57
62
  @dataclass
@@ -74,13 +79,16 @@ class AttributeFromDB:
74
79
  value: Any
75
80
  content: Any
76
81
 
77
- updated_at: str
78
-
79
82
  branch: str
80
83
 
81
84
  is_default: bool
82
85
  is_from_profile: bool = dataclass_field(default=False)
83
86
 
87
+ updated_at: Timestamp | None = None
88
+ updated_by: str | None = None
89
+ created_at: Timestamp | None = None
90
+ created_by: str | None = None
91
+
84
92
  node_properties: dict[str, AttributeNodePropertyFromDB] = dataclass_field(default_factory=dict)
85
93
  flag_properties: dict[str, bool] = dataclass_field(default_factory=dict)
86
94
 
@@ -108,8 +116,6 @@ class NodeQuery(Query):
108
116
  branch: Branch | None = None,
109
117
  **kwargs,
110
118
  ) -> None:
111
- # TODO Validate that Node is a valid node
112
- # Eventually extract the branch from Node as well
113
119
  self.node = node
114
120
  self.node_id = node_id or id
115
121
  self.node_db_id = node_db_id
@@ -133,6 +139,7 @@ class NodeCreateAllQuery(NodeQuery):
133
139
 
134
140
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002, PLR0915
135
141
  at = self.at or self.node._at
142
+ self.params["user_id"] = self.user_id
136
143
  self.params["uuid"] = self.node.id
137
144
  self.params["branch"] = self.branch.name
138
145
  self.params["branch_level"] = self.branch.hierarchy_level
@@ -188,7 +195,7 @@ class NodeCreateAllQuery(NodeQuery):
188
195
  pass
189
196
  except ValueError:
190
197
  # Relationship has not been initialized yet, it means the peer does not exist in db yet
191
- # typically because it will be allocated from a ressource pool. In that case, the peer
198
+ # typically because it will be allocated from a resource pool. In that case, the peer
192
199
  # will be fetched using `rel.resolve` later.
193
200
  pass
194
201
 
@@ -220,14 +227,40 @@ class NodeCreateAllQuery(NodeQuery):
220
227
  "namespace": self.node._schema.namespace,
221
228
  "branch_support": self.node._schema.branch,
222
229
  }
230
+ if self.branch.is_default or self.branch.is_global:
231
+ self.params["node_prop"].update(
232
+ {
233
+ "created_at": at.to_string(),
234
+ "created_by": self.user_id,
235
+ "updated_at": at.to_string(),
236
+ "updated_by": self.user_id,
237
+ }
238
+ )
223
239
  self.params["node_branch_prop"] = {
224
240
  "branch": self.branch.name,
225
241
  "branch_level": self.branch.hierarchy_level,
226
242
  "status": "active",
227
243
  "from": at.to_string(),
244
+ "from_user_id": self.user_id,
228
245
  }
229
246
 
230
- rel_prop_str = "{ branch: rel.branch, branch_level: rel.branch_level, status: rel.status, hierarchy: rel.hierarchical, from: $at }"
247
+ # set all the property strings that we reuse
248
+ # include the create/updated_at/by metadata if on default or global branch
249
+ attr_edge_prop_str = "{ branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at, from_user_id: $user_id }"
250
+ attr_vertex_prop_str = "{ uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support"
251
+ if self.branch.is_default or self.branch.is_global:
252
+ attr_vertex_prop_str += ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
253
+ attr_vertex_prop_str += " }"
254
+
255
+ rel_edge_prop_str = "{ branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at, from_user_id: $user_id }"
256
+ rel_edge_prop_str_hierarchy = (
257
+ "{ branch: rel.branch, branch_level: rel.branch_level, "
258
+ "status: rel.status, hierarchy: rel.hierarchical, from: $at, from_user_id: $user_id }"
259
+ )
260
+ rel_vertex_prop_str = "{ uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support"
261
+ if self.branch.is_default or self.branch.is_global:
262
+ rel_vertex_prop_str += ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
263
+ rel_vertex_prop_str += " }"
231
264
 
232
265
  iphost_prop = {
233
266
  "value": "attr.content.value",
@@ -270,102 +303,102 @@ class NodeCreateAllQuery(NodeQuery):
270
303
  LIMIT 1
271
304
  }
272
305
  CALL (n, attr, av) {
273
- CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
274
- CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
275
- CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
306
+ CREATE (a:Attribute %(attr_vertex)s)
307
+ CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
308
+ CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
276
309
  MERGE (ip:Boolean { value: attr.is_protected })
277
- MERGE (iv:Boolean { value: attr.is_visible })
278
- WITH a, ip, iv
310
+ WITH a, ip
279
311
  LIMIT 1
280
- CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
281
- CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
312
+ CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
282
313
  FOREACH ( prop IN attr.source_prop |
283
314
  MERGE (peer:Node { uuid: prop.peer_id })
284
- CREATE (a)-[:HAS_SOURCE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
315
+ CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
285
316
  )
286
317
  FOREACH ( prop IN attr.owner_prop |
287
318
  MERGE (peer:Node { uuid: prop.peer_id })
288
- CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
319
+ CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
289
320
  )
290
- }"""
321
+ }""" % {"attr_edge": attr_edge_prop_str, "attr_vertex": attr_vertex_prop_str}
291
322
 
292
323
  attrs_indexed_query = """
293
324
  WITH distinct n
294
325
  UNWIND $attrs_indexed AS attr
295
326
  CALL (n, attr) {
296
- CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
297
- CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
327
+ CREATE (a:Attribute %(attr_vertex)s)
328
+ CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
298
329
  MERGE (av:AttributeValue:AttributeValueIndexed { value: attr.content.value, is_default: attr.content.is_default })
299
330
  WITH av, a
300
331
  LIMIT 1
301
- CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
332
+ CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
302
333
  MERGE (ip:Boolean { value: attr.is_protected })
303
- MERGE (iv:Boolean { value: attr.is_visible })
304
- CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
305
- CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
334
+ CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
306
335
  FOREACH ( prop IN attr.source_prop |
307
336
  MERGE (peer:Node { uuid: prop.peer_id })
308
- CREATE (a)-[:HAS_SOURCE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
337
+ CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
309
338
  )
310
339
  FOREACH ( prop IN attr.owner_prop |
311
340
  MERGE (peer:Node { uuid: prop.peer_id })
312
- CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
341
+ CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
313
342
  )
314
- }"""
343
+ }""" % {"attr_edge": attr_edge_prop_str, "attr_vertex": attr_vertex_prop_str}
315
344
 
316
345
  attrs_iphost_query = """
317
346
  WITH distinct n
318
347
  UNWIND $attrs_iphost AS attr
319
348
  CALL (n, attr) {
320
- CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
321
- CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
349
+ CREATE (a:Attribute %(attr_vertex)s)
350
+ CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
322
351
  MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPHost { %(iphost_prop)s })
323
352
  WITH attr, av, a
324
353
  LIMIT 1
325
- CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
354
+ CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
326
355
  MERGE (ip:Boolean { value: attr.is_protected })
327
- MERGE (iv:Boolean { value: attr.is_visible })
328
- WITH a, ip, iv
356
+ WITH a, ip
329
357
  LIMIT 1
330
- CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
331
- CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
358
+ CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
332
359
  FOREACH ( prop IN attr.source_prop |
333
360
  MERGE (peer:Node { uuid: prop.peer_id })
334
- CREATE (a)-[:HAS_SOURCE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
361
+ CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
335
362
  )
336
363
  FOREACH ( prop IN attr.owner_prop |
337
364
  MERGE (peer:Node { uuid: prop.peer_id })
338
- CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
365
+ CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
339
366
  )
340
367
  }
341
- """ % {"iphost_prop": ", ".join(iphost_prop_list)}
368
+ """ % {
369
+ "iphost_prop": ", ".join(iphost_prop_list),
370
+ "attr_edge": attr_edge_prop_str,
371
+ "attr_vertex": attr_vertex_prop_str,
372
+ }
342
373
 
343
374
  attrs_ipnetwork_query = """
344
375
  WITH distinct n
345
376
  UNWIND $attrs_ipnetwork AS attr
346
377
  CALL (n, attr) {
347
- CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
348
- CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
378
+ CREATE (a:Attribute %(attr_vertex)s)
379
+ CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
349
380
  MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPNetwork { %(ipnetwork_prop)s })
350
381
  WITH attr, av, a
351
382
  LIMIT 1
352
- CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
383
+ CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
353
384
  MERGE (ip:Boolean { value: attr.is_protected })
354
- MERGE (iv:Boolean { value: attr.is_visible })
355
- WITH a, ip, iv
385
+ WITH a, ip
356
386
  LIMIT 1
357
- CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
358
- CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
387
+ CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
359
388
  FOREACH ( prop IN attr.source_prop |
360
389
  MERGE (peer:Node { uuid: prop.peer_id })
361
- CREATE (a)-[:HAS_SOURCE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
390
+ CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
362
391
  )
363
392
  FOREACH ( prop IN attr.owner_prop |
364
393
  MERGE (peer:Node { uuid: prop.peer_id })
365
- CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
394
+ CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
366
395
  )
367
396
  }
368
- """ % {"ipnetwork_prop": ", ".join(ipnetwork_prop_list)}
397
+ """ % {
398
+ "ipnetwork_prop": ", ".join(ipnetwork_prop_list),
399
+ "attr_edge": attr_edge_prop_str,
400
+ "attr_vertex": attr_vertex_prop_str,
401
+ }
369
402
 
370
403
  deepest_branch = await registry.get_branch(db=db, branch=deepest_branch_name)
371
404
  branch_filter, branch_params = deepest_branch.get_query_filter_path(at=self.at)
@@ -409,75 +442,84 @@ class NodeCreateAllQuery(NodeQuery):
409
442
  UNWIND $rels_bidir AS rel
410
443
  %(dest_node_subquery)s
411
444
  CALL (n, rel, dest_node) {
412
- CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
413
- CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
414
- CREATE (dest_node)-[:IS_RELATED %(rel_prop)s ]->(rl)
445
+ CREATE (rl:Relationship %(rel_vertex)s)
446
+ CREATE (n)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
447
+ CREATE (dest_node)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
415
448
  MERGE (ip:Boolean { value: rel.is_protected })
416
- MERGE (iv:Boolean { value: rel.is_visible })
417
- WITH rl, ip, iv
449
+ WITH rl, ip
418
450
  LIMIT 1
419
- CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
420
- CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
451
+ CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
421
452
  FOREACH ( prop IN rel.source_prop |
422
453
  MERGE (peer:Node { uuid: prop.peer_id })
423
- CREATE (rl)-[:HAS_SOURCE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
454
+ CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
424
455
  )
425
456
  FOREACH ( prop IN rel.owner_prop |
426
457
  MERGE (peer:Node { uuid: prop.peer_id })
427
- CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
458
+ CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
428
459
  )
429
460
  }
430
- """ % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
461
+ """ % {
462
+ "rel_edge": rel_edge_prop_str,
463
+ "rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
464
+ "rel_vertex": rel_vertex_prop_str,
465
+ "dest_node_subquery": dest_node_subquery,
466
+ }
431
467
 
432
468
  rels_out_query = """
433
469
  WITH distinct n
434
470
  UNWIND $rels_out AS rel
435
471
  %(dest_node_subquery)s
436
472
  CALL (n, rel, dest_node) {
437
- CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
438
- CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
439
- CREATE (dest_node)<-[:IS_RELATED %(rel_prop)s ]-(rl)
473
+ CREATE (rl:Relationship %(rel_vertex)s)
474
+ CREATE (n)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
475
+ CREATE (dest_node)<-[:IS_RELATED %(rel_edge_hierarchy)s ]-(rl)
440
476
  MERGE (ip:Boolean { value: rel.is_protected })
441
- MERGE (iv:Boolean { value: rel.is_visible })
442
- WITH rl, ip, iv
477
+ WITH rl, ip
443
478
  LIMIT 1
444
- CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
445
- CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
479
+ CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
446
480
  FOREACH ( prop IN rel.source_prop |
447
481
  MERGE (peer:Node { uuid: prop.peer_id })
448
- CREATE (rl)-[:HAS_SOURCE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
482
+ CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
449
483
  )
450
484
  FOREACH ( prop IN rel.owner_prop |
451
485
  MERGE (peer:Node { uuid: prop.peer_id })
452
- CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
486
+ CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
453
487
  )
454
488
  }
455
- """ % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
489
+ """ % {
490
+ "rel_edge": rel_edge_prop_str,
491
+ "rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
492
+ "rel_vertex": rel_vertex_prop_str,
493
+ "dest_node_subquery": dest_node_subquery,
494
+ }
456
495
 
457
496
  rels_in_query = """
458
497
  WITH distinct n
459
498
  UNWIND $rels_in AS rel
460
499
  %(dest_node_subquery)s
461
500
  CALL (n, rel, dest_node) {
462
- CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
463
- CREATE (n)<-[:IS_RELATED %(rel_prop)s ]-(rl)
464
- CREATE (dest_node)-[:IS_RELATED %(rel_prop)s ]->(rl)
501
+ CREATE (rl:Relationship %(rel_vertex)s)
502
+ CREATE (n)<-[:IS_RELATED %(rel_edge_hierarchy)s ]-(rl)
503
+ CREATE (dest_node)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
465
504
  MERGE (ip:Boolean { value: rel.is_protected })
466
- MERGE (iv:Boolean { value: rel.is_visible })
467
- WITH rl, ip, iv
505
+ WITH rl, ip
468
506
  LIMIT 1
469
- CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
470
- CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
507
+ CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
471
508
  FOREACH ( prop IN rel.source_prop |
472
509
  MERGE (peer:Node { uuid: prop.peer_id })
473
- CREATE (rl)-[:HAS_SOURCE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
510
+ CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
474
511
  )
475
512
  FOREACH ( prop IN rel.owner_prop |
476
513
  MERGE (peer:Node { uuid: prop.peer_id })
477
- CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
514
+ CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
478
515
  )
479
516
  }
480
- """ % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
517
+ """ % {
518
+ "rel_edge": rel_edge_prop_str,
519
+ "rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
520
+ "rel_vertex": rel_vertex_prop_str,
521
+ "dest_node_subquery": dest_node_subquery,
522
+ }
481
523
 
482
524
  query = f"""
483
525
  MATCH (root:Root)
@@ -526,48 +568,83 @@ class NodeCreateAllQuery(NodeQuery):
526
568
 
527
569
  class NodeDeleteQuery(NodeQuery):
528
570
  name = "node_delete"
529
-
530
571
  type: QueryType = QueryType.WRITE
531
-
532
- raise_error_if_empty: bool = True
572
+ insert_return = False
573
+ raise_error_if_empty = False
533
574
 
534
575
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
576
+ self.params["user_id"] = self.user_id
535
577
  self.params["uuid"] = self.node_id
536
578
  self.params["branch"] = self.branch.name
537
579
  self.params["branch_level"] = self.branch.hierarchy_level
580
+ self.params["at"] = self.at.to_string()
538
581
 
539
582
  if self.branch.is_global or self.branch.is_default:
583
+ # update the updated_at/by metadata on the Node if we're on the global or default branch
540
584
  node_query_match = """
541
- MATCH (n:Node { uuid: $uuid })
542
- OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
543
- WHERE delete_edge.from <= $at
544
- WITH n WHERE delete_edge IS NULL
585
+ MATCH (n:Node { uuid: $uuid })-[r:IS_PART_OF { branch_level: 1, status: "active" }]->(:Root)
586
+ WHERE r.to IS NULL
587
+ OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
588
+ WHERE delete_edge.from <= $at
589
+ WITH n, r
590
+ WHERE delete_edge IS NULL
591
+ SET n.updated_at = $at, n.updated_by = $user_id
592
+ WITH n, r
545
593
  """
546
594
  else:
547
595
  node_filter, node_filter_params = self.branch.get_query_filter_path(at=self.at, variable_name="r")
548
596
  node_query_match = """
549
- MATCH (n:Node { uuid: $uuid })
550
- CALL (n) {
551
- MATCH (n)-[r:IS_PART_OF]->(:Root)
552
- WHERE %(node_filter)s
553
- RETURN r.status = "active" AS is_active
554
- ORDER BY r.from DESC
555
- LIMIT 1
556
- }
557
- WITH n WHERE is_active = TRUE
597
+ MATCH (n:Node { uuid: $uuid })
598
+ CALL (n) {
599
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
600
+ WHERE %(node_filter)s
601
+ RETURN r
602
+ ORDER BY r.from DESC
603
+ LIMIT 1
604
+ }
605
+ WITH n, r
606
+ WHERE r.status = "active"
558
607
  """ % {"node_filter": node_filter}
559
608
  self.params.update(node_filter_params)
560
609
  self.add_to_query(node_query_match)
561
610
 
611
+ # set the to time/user_id if the active IS_PART_OF edge is on this branch
562
612
  query = """
563
- MATCH (root:Root)
564
- CREATE (n)-[r:IS_PART_OF { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at }]->(root)
613
+ MATCH (root:Root)
614
+ LIMIT 1
615
+ CREATE (n)-[delete_edge:IS_PART_OF { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]->(root)
616
+ WITH r
617
+ WHERE r.branch = $branch
618
+ SET r.to = $at
619
+ SET r.to_user_id = $user_id
565
620
  """
621
+ self.add_to_query(query)
566
622
 
623
+
624
+ class NodeUpdateMetadataQuery(NodeQuery):
625
+ name = "node_update_metadata"
626
+ type: QueryType = QueryType.WRITE
627
+ insert_return = False
628
+ raise_error_if_empty = False
629
+
630
+ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
631
+ if not self.branch.is_default and not self.branch.is_global:
632
+ raise ValueError("NodeUpdateMetadataQuery can only be used on the default or global branch")
633
+ self.params["uuid"] = self.node_id
634
+ self.params["branch"] = self.branch.name
567
635
  self.params["at"] = self.at.to_string()
636
+ self.params["user_id"] = self.user_id
568
637
 
638
+ query = """
639
+ MATCH (n:Node { uuid: $uuid })-[r:IS_PART_OF { branch_level: 1, status: "active" }]->(:Root)
640
+ WHERE r.to IS NULL
641
+ OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
642
+ WHERE delete_edge.from <= $at
643
+ WITH n, r
644
+ WHERE delete_edge IS NULL
645
+ SET n.updated_at = $at, n.updated_by = $user_id
646
+ """
569
647
  self.add_to_query(query)
570
- self.return_labels = ["n"]
571
648
 
572
649
 
573
650
  class NodeCheckIDQuery(Query):
@@ -603,30 +680,140 @@ class NodeListGetAttributeQuery(Query):
603
680
  "HAS_OWNER": ("rel_owner", "owner"),
604
681
  "HAS_SOURCE": ("rel_source", "source"),
605
682
  "IS_PROTECTED": ("rel_isp", "isp"),
606
- "IS_VISIBLE": ("rel_isv", "isv"),
607
683
  }
608
684
 
609
685
  def __init__(
610
686
  self,
611
687
  ids: list[str],
612
688
  fields: dict | None = None,
613
- include_source: bool = False,
614
- include_owner: bool = False,
615
- account=None,
689
+ include_metadata: MetadataOptions = MetadataOptions.NONE,
616
690
  **kwargs,
617
691
  ):
618
- self.account = account
619
692
  self.ids = ids
620
693
  self.fields = fields
621
- self.include_source = include_source
622
- self.include_owner = include_owner
623
-
694
+ self.include_metadata = include_metadata
624
695
  super().__init__(order_by=["n.uuid", "a.name"], **kwargs)
625
696
 
697
+ @property
698
+ def _include_source(self) -> bool:
699
+ return bool(self.include_metadata & MetadataOptions.SOURCE)
700
+
701
+ @property
702
+ def _include_owner(self) -> bool:
703
+ return bool(self.include_metadata & MetadataOptions.OWNER)
704
+
705
+ @property
706
+ def _include_updated_metadata(self) -> bool:
707
+ return bool(self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY))
708
+
709
+ @property
710
+ def _include_created_metadata(self) -> bool:
711
+ return bool(self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY))
712
+
713
+ def _add_source_to_query(self, branch_filter_str: str) -> None:
714
+ if not self._include_source:
715
+ return
716
+ source_query = """
717
+ CALL (a) {
718
+ OPTIONAL MATCH (a)-[rel_source:HAS_SOURCE]-(source)
719
+ WHERE all(r IN [rel_source] WHERE ( %(branch_filter)s ))
720
+ RETURN source, rel_source
721
+ ORDER BY rel_source.branch_level DESC, rel_source.from DESC, rel_source.status ASC
722
+ LIMIT 1
723
+ }
724
+ WITH *,
725
+ CASE WHEN rel_source.status = "active" THEN source ELSE NULL END AS source,
726
+ CASE WHEN rel_source.status = "active" THEN rel_source ELSE NULL END AS rel_source
727
+ """ % {"branch_filter": branch_filter_str}
728
+ self.add_to_query(source_query)
729
+ self.return_labels.extend(["source", "rel_source"])
730
+
731
+ def _add_owner_to_query(self, branch_filter_str: str) -> None:
732
+ if not self._include_owner:
733
+ return
734
+ owner_query = """
735
+ CALL (a) {
736
+ OPTIONAL MATCH (a)-[rel_owner:HAS_OWNER]-(owner)
737
+ WHERE all(r IN [rel_owner] WHERE ( %(branch_filter)s ))
738
+ RETURN owner, rel_owner
739
+ ORDER BY rel_owner.branch_level DESC, rel_owner.from DESC, rel_owner.status ASC
740
+ LIMIT 1
741
+ }
742
+ WITH *,
743
+ CASE WHEN rel_owner.status = "active" THEN owner ELSE NULL END AS owner,
744
+ CASE WHEN rel_owner.status = "active" THEN rel_owner ELSE NULL END AS rel_owner
745
+ """ % {"branch_filter": branch_filter_str}
746
+ self.add_to_query(owner_query)
747
+ self.return_labels.extend(["owner", "rel_owner"])
748
+
749
+ def _add_created_metadata_to_query(self) -> None:
750
+ if not self._include_created_metadata:
751
+ return
752
+ if self.branch.is_default or self.branch.is_global:
753
+ last_created_query = """
754
+ WITH *, a.created_at AS created_at, a.created_by AS created_by
755
+ """
756
+ else:
757
+ last_created_query = """
758
+ CALL (a) {
759
+ MATCH ()-[e:HAS_ATTRIBUTE {status: "active"}]->(a)
760
+ RETURN e.from AS created_at, e.from_user_id AS created_by
761
+ ORDER BY e.from ASC
762
+ LIMIT 1
763
+ }
764
+ """
765
+ self.add_to_query(last_created_query)
766
+ self.return_labels.extend(["created_at", "created_by"])
767
+
768
+ def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
769
+ if not self._include_updated_metadata:
770
+ return
771
+ if self.branch.is_default or self.branch.is_global:
772
+ last_updated_query = """
773
+ WITH *, a.updated_at AS updated_at, a.updated_by AS updated_by
774
+ """
775
+ else:
776
+ if self.branch_agnostic:
777
+ time_details = """
778
+ WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
779
+ """
780
+ else:
781
+ time_details = """
782
+ WITH CASE
783
+ WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
784
+ WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
785
+ ELSE [NULL, NULL]
786
+ END AS from_details,
787
+ CASE
788
+ WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
789
+ WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
790
+ ELSE [NULL, NULL]
791
+ END AS to_details
792
+ """
793
+ last_updated_query = """
794
+ CALL (a) {
795
+ MATCH (a)-[r]->(property)
796
+ WHERE %(branch_filter)s
797
+ %(time_details)s
798
+ WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
799
+ WITH from_details_list + to_details_list AS details_list
800
+ UNWIND details_list AS one_details
801
+ WITH one_details[0] AS updated_at, one_details[1] AS updated_by
802
+ WHERE updated_at IS NOT NULL
803
+ WITH updated_at, updated_by
804
+ ORDER BY updated_at DESC
805
+ LIMIT 1
806
+ RETURN updated_at, updated_by
807
+ }
808
+ """ % {"branch_filter": branch_filter_str, "time_details": time_details}
809
+ self.add_to_query(last_updated_query)
810
+ self.return_labels.extend(["updated_at", "updated_by"])
811
+
626
812
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
627
813
  self.params["ids"] = self.ids
628
814
  self.params["profile_node_relationship_name"] = PROFILE_NODE_RELATIONSHIP_IDENTIFIER
629
815
  self.params["profile_template_relationship_name"] = PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER
816
+ self.params["field_names"] = list(self.fields.keys()) if self.fields else []
630
817
 
631
818
  branch_filter, branch_params = self.branch.get_query_filter_path(
632
819
  at=self.at, branch_agnostic=self.branch_agnostic
@@ -640,11 +827,9 @@ class NodeListGetAttributeQuery(Query):
640
827
  exists((n)-[:IS_RELATED]-(:Relationship {name: $profile_template_relationship_name}))
641
828
  ) AS might_use_profile
642
829
  MATCH (n)-[:HAS_ATTRIBUTE]-(a:Attribute)
830
+ WHERE (a.name IN $field_names OR size($field_names) = 0)
831
+ WITH DISTINCT n, a, might_use_profile
643
832
  """
644
- if self.fields:
645
- query += "\n WHERE a.name IN $field_names"
646
- self.params["field_names"] = list(self.fields.keys())
647
-
648
833
  self.add_to_query(query)
649
834
 
650
835
  query = """
@@ -680,15 +865,8 @@ WHERE r2.status = "active"
680
865
 
681
866
  self.return_labels = ["n", "a", "av", "r1", "r2", "is_from_profile"]
682
867
 
683
- # Add Is_Protected and Is_visible
868
+ # Add Is_Protected
684
869
  query = """
685
- CALL (a) {
686
- MATCH (a)-[r:IS_VISIBLE]-(isv:Boolean)
687
- WHERE (%(branch_filter)s)
688
- RETURN r AS rel_isv, isv
689
- ORDER BY rel_isv.branch_level DESC, rel_isv.from DESC, rel_isv.status ASC
690
- LIMIT 1
691
- }
692
870
  CALL (a) {
693
871
  MATCH (a)-[r:IS_PROTECTED]-(isp:Boolean)
694
872
  WHERE (%(branch_filter)s)
@@ -699,39 +877,12 @@ CALL (a) {
699
877
  """ % {"branch_filter": branch_filter}
700
878
  self.add_to_query(query)
701
879
 
702
- self.return_labels.extend(["isv", "isp", "rel_isv", "rel_isp"])
880
+ self.return_labels.extend(["isp", "rel_isp"])
703
881
 
704
- if self.include_source:
705
- query = """
706
- CALL (a) {
707
- OPTIONAL MATCH (a)-[rel_source:HAS_SOURCE]-(source)
708
- WHERE all(r IN [rel_source] WHERE ( %(branch_filter)s ))
709
- RETURN source, rel_source
710
- ORDER BY rel_source.branch_level DESC, rel_source.from DESC, rel_source.status ASC
711
- LIMIT 1
712
- }
713
- WITH *,
714
- CASE WHEN rel_source.status = "active" THEN source ELSE NULL END AS source,
715
- CASE WHEN rel_source.status = "active" THEN rel_source ELSE NULL END AS rel_source
716
- """ % {"branch_filter": branch_filter}
717
- self.add_to_query(query)
718
- self.return_labels.extend(["source", "rel_source"])
719
-
720
- if self.include_owner:
721
- query = """
722
- CALL (a) {
723
- OPTIONAL MATCH (a)-[rel_owner:HAS_OWNER]-(owner)
724
- WHERE all(r IN [rel_owner] WHERE ( %(branch_filter)s ))
725
- RETURN owner, rel_owner
726
- ORDER BY rel_owner.branch_level DESC, rel_owner.from DESC, rel_owner.status ASC
727
- LIMIT 1
728
- }
729
- WITH *,
730
- CASE WHEN rel_owner.status = "active" THEN owner ELSE NULL END AS owner,
731
- CASE WHEN rel_owner.status = "active" THEN rel_owner ELSE NULL END AS rel_owner
732
- """ % {"branch_filter": branch_filter}
733
- self.add_to_query(query)
734
- self.return_labels.extend(["owner", "rel_owner"])
882
+ self._add_source_to_query(branch_filter_str=branch_filter)
883
+ self._add_owner_to_query(branch_filter_str=branch_filter)
884
+ self._add_created_metadata_to_query()
885
+ self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
735
886
 
736
887
  def get_attributes_group_by_node(self) -> dict[str, NodeAttributesFromDB]:
737
888
  attrs_by_node: dict[str, NodeAttributesFromDB] = {}
@@ -768,7 +919,6 @@ CALL (a) {
768
919
  attr_uuid=attr.get("uuid"),
769
920
  attr_value_id=attr_value.element_id,
770
921
  attr_value_uuid=attr_value.get("uuid"),
771
- updated_at=result.get_rel("r2").get("from"),
772
922
  value=attr_value.get("value"),
773
923
  is_default=attr_value.get("is_default"),
774
924
  is_from_profile=is_from_profile,
@@ -776,16 +926,26 @@ CALL (a) {
776
926
  branch=self.branch.name,
777
927
  flag_properties={
778
928
  "is_protected": result.get("isp").get("value"),
779
- "is_visible": result.get("isv").get("value"),
780
929
  },
781
930
  )
782
931
 
783
- if self.include_source and result.get("source"):
932
+ if self.include_metadata & MetadataOptions.CREATED_AT:
933
+ created_at_str = result.get_as_str("created_at")
934
+ data.created_at = Timestamp(created_at_str) if created_at_str else None
935
+ if self.include_metadata & MetadataOptions.CREATED_BY:
936
+ data.created_by = result.get_as_str("created_by")
937
+ if self.include_metadata & MetadataOptions.UPDATED_AT:
938
+ updated_at_str = result.get_as_str("updated_at")
939
+ data.updated_at = Timestamp(updated_at_str) if updated_at_str else None
940
+ if self.include_metadata & MetadataOptions.UPDATED_BY:
941
+ data.updated_by = result.get_as_str("updated_by")
942
+
943
+ if self._include_source and result.get("source"):
784
944
  data.node_properties["source"] = AttributeNodePropertyFromDB(
785
945
  uuid=result.get_node("source").get("uuid"), labels=list(result.get_node("source").labels)
786
946
  )
787
947
 
788
- if self.include_owner and result.get("owner"):
948
+ if self._include_owner and result.get("owner"):
789
949
  data.node_properties["owner"] = AttributeNodePropertyFromDB(
790
950
  uuid=result.get_node("owner").get("uuid"), labels=list(result.get_node("owner").labels)
791
951
  )
@@ -799,12 +959,37 @@ class GroupedPeerNodes:
799
959
  self._rel_names_by_node_id: dict[str, set[str]] = defaultdict(set)
800
960
  # {(node_id, rel_name): {RelationshipDirection: {peer_id, ...}}}
801
961
  self._rel_directions_map: dict[tuple[str, str], dict[RelationshipDirection, set[str]]] = defaultdict(dict)
962
+ # {(node_id, rel_name, direction, peer_Id): {MetadataOptions: value}}
963
+ self._metadata_map: dict[
964
+ tuple[str, str, RelationshipDirection, str], dict[MetadataOptions, Timestamp | str | None]
965
+ ] = {}
802
966
 
803
- def add_peer(self, node_id: str, rel_name: str, peer_id: str, direction: RelationshipDirection) -> None:
967
+ def add_peer(
968
+ self,
969
+ node_id: str,
970
+ rel_name: str,
971
+ peer_id: str,
972
+ direction: RelationshipDirection,
973
+ created_at: Timestamp | None = None,
974
+ created_by: str | None = None,
975
+ updated_at: Timestamp | None = None,
976
+ updated_by: str | None = None,
977
+ ) -> None:
804
978
  self._rel_names_by_node_id[node_id].add(rel_name)
805
979
  if direction not in self._rel_directions_map[node_id, rel_name]:
806
980
  self._rel_directions_map[node_id, rel_name][direction] = set()
807
981
  self._rel_directions_map[node_id, rel_name][direction].add(peer_id)
982
+ key = (node_id, rel_name, direction, peer_id)
983
+ if created_at is not None or created_by is not None or updated_at is not None or updated_by is not None:
984
+ self._metadata_map[key] = {}
985
+ if created_at is not None:
986
+ self._metadata_map[key][MetadataOptions.CREATED_AT] = created_at
987
+ if created_by is not None:
988
+ self._metadata_map[key][MetadataOptions.CREATED_BY] = created_by
989
+ if updated_at is not None:
990
+ self._metadata_map[key][MetadataOptions.UPDATED_AT] = updated_at
991
+ if updated_by is not None:
992
+ self._metadata_map[key][MetadataOptions.UPDATED_BY] = updated_by
808
993
 
809
994
  def get_peer_ids(self, node_id: str, rel_name: str, direction: RelationshipDirection) -> set[str]:
810
995
  if (node_id, rel_name) not in self._rel_directions_map:
@@ -821,11 +1006,15 @@ class GroupedPeerNodes:
821
1006
  def has_node(self, node_id: str) -> bool:
822
1007
  return node_id in self._rel_names_by_node_id
823
1008
 
1009
+ def get_metadata_map(
1010
+ self, node_id: str, rel_name: str, direction: RelationshipDirection, peer_id: str
1011
+ ) -> dict[MetadataOptions, Timestamp | str | None]:
1012
+ return self._metadata_map.get((node_id, rel_name, direction, peer_id), {})
1013
+
824
1014
 
825
1015
  class NodeListGetRelationshipsQuery(Query):
826
1016
  name: str = "node_list_get_relationship"
827
1017
  type: QueryType = QueryType.READ
828
- insert_return: bool = False
829
1018
 
830
1019
  def __init__(
831
1020
  self,
@@ -833,14 +1022,80 @@ class NodeListGetRelationshipsQuery(Query):
833
1022
  outbound_identifiers: list[str] | None = None,
834
1023
  inbound_identifiers: list[str] | None = None,
835
1024
  bidirectional_identifiers: list[str] | None = None,
1025
+ include_metadata: MetadataOptions = MetadataOptions.NONE,
836
1026
  **kwargs,
837
1027
  ):
838
1028
  self.ids = ids
839
1029
  self.outbound_identifiers = outbound_identifiers
840
1030
  self.inbound_identifiers = inbound_identifiers
841
1031
  self.bidirectional_identifiers = bidirectional_identifiers
1032
+ self.include_metadata = include_metadata
842
1033
  super().__init__(**kwargs)
843
1034
 
1035
+ def _add_created_metadata_to_query(self) -> None:
1036
+ if not (self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY)):
1037
+ return
1038
+ if self.branch.is_default or self.branch.is_global:
1039
+ last_created_query = """
1040
+ WITH *, rel.created_at AS created_at, rel.created_by AS created_by
1041
+ """
1042
+ else:
1043
+ last_created_query = """
1044
+ WITH *, CASE
1045
+ WHEN r1.from < r2.from THEN [r1.from, r1.from_user_id]
1046
+ ELSE [r2.from, r2.from_user_id]
1047
+ END AS created_details
1048
+ WITH *, created_details[0] AS created_at, created_details[1] AS created_by
1049
+ """
1050
+ self.add_to_query(last_created_query)
1051
+ self.return_labels.extend(["created_at", "created_by"])
1052
+
1053
+ def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
1054
+ if not (self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY)):
1055
+ return
1056
+ if self.branch.is_default or self.branch.is_global:
1057
+ last_updated_query = """
1058
+ WITH *, rel.updated_at AS updated_at, rel.updated_by AS updated_by
1059
+ """
1060
+ else:
1061
+ if self.branch_agnostic:
1062
+ time_details = """
1063
+ WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
1064
+ """
1065
+ else:
1066
+ time_details = """
1067
+ WITH CASE
1068
+ WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
1069
+ WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
1070
+ ELSE [NULL, NULL]
1071
+ END AS from_details,
1072
+ CASE
1073
+ WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
1074
+ WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
1075
+ ELSE [NULL, NULL]
1076
+ END AS to_details
1077
+ """
1078
+ last_updated_query = """
1079
+ CALL (rel) {
1080
+ // don't use IS_RELATED edges to handle the case when at least one of the
1081
+ // peers is a migrated-kind node
1082
+ MATCH (rel)-[r:!IS_RELATED]->(property)
1083
+ WHERE %(branch_filter)s
1084
+ %(time_details)s
1085
+ WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
1086
+ WITH from_details_list + to_details_list AS details_list
1087
+ UNWIND details_list AS one_details
1088
+ WITH one_details[0] AS updated_at, one_details[1] AS updated_by
1089
+ WHERE updated_at IS NOT NULL
1090
+ WITH updated_at, updated_by
1091
+ ORDER BY updated_at DESC
1092
+ LIMIT 1
1093
+ RETURN updated_at, updated_by
1094
+ }
1095
+ """ % {"branch_filter": branch_filter_str, "time_details": time_details}
1096
+ self.add_to_query(last_updated_query)
1097
+ self.return_labels.extend(["updated_at", "updated_by"])
1098
+
844
1099
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
845
1100
  self.params["ids"] = self.ids
846
1101
  self.params["outbound_identifiers"] = self.outbound_identifiers
@@ -872,9 +1127,9 @@ class NodeListGetRelationshipsQuery(Query):
872
1127
  LIMIT 1
873
1128
  WITH r1, r AS r2
874
1129
  WHERE r2.status = "active"
875
- RETURN 1 AS is_active
1130
+ RETURN r1, r2
876
1131
  }
877
- RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "inbound" as direction
1132
+ RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "inbound" as direction, r1, r2
878
1133
  UNION
879
1134
  WITH n
880
1135
  MATCH (n)-[:IS_RELATED]->(rel:Relationship)-[:IS_RELATED]->(peer)
@@ -896,9 +1151,9 @@ class NodeListGetRelationshipsQuery(Query):
896
1151
  LIMIT 1
897
1152
  WITH r1, r AS r2
898
1153
  WHERE r2.status = "active"
899
- RETURN 1 AS is_active
1154
+ RETURN r1, r2
900
1155
  }
901
- RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "outbound" as direction
1156
+ RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "outbound" as direction, r1, r2
902
1157
  UNION
903
1158
  WITH n
904
1159
  MATCH (n)-[:IS_RELATED]->(rel:Relationship)<-[:IS_RELATED]-(peer)
@@ -920,15 +1175,21 @@ class NodeListGetRelationshipsQuery(Query):
920
1175
  LIMIT 1
921
1176
  WITH r1, r AS r2
922
1177
  WHERE r2.status = "active"
923
- RETURN 1 AS is_active
1178
+ RETURN r1, r2
924
1179
  }
925
- RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "bidirectional" as direction
1180
+ RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "bidirectional" as direction, r1, r2
926
1181
  }
927
- RETURN DISTINCT n_uuid, rel_name, peer_uuid, direction
928
1182
  """ % {"filters": rels_filter}
929
1183
  self.add_to_query(query)
1184
+
930
1185
  self.order_by = ["n_uuid", "rel_name", "peer_uuid", "direction"]
931
- self.return_labels = ["n_uuid", "rel_name", "peer_uuid", "direction"]
1186
+ self.return_labels = ["n_uuid", "peer_uuid", "direction"]
1187
+
1188
+ self._add_created_metadata_to_query()
1189
+ self._add_updated_metadata_to_query(branch_filter_str=rels_filter)
1190
+ return_labels_str = ", ".join(sorted(self.return_labels))
1191
+ self.add_to_query(f"WITH DISTINCT {return_labels_str}, rel.name AS rel_name")
1192
+ self.return_labels.append("rel_name")
932
1193
 
933
1194
  def get_peers_group_by_node(self) -> GroupedPeerNodes:
934
1195
  gpn = GroupedPeerNodes()
@@ -937,12 +1198,40 @@ class NodeListGetRelationshipsQuery(Query):
937
1198
  rel_name = result.get("rel_name")
938
1199
  peer_id = result.get("peer_uuid")
939
1200
  direction = str(result.get("direction"))
1201
+
1202
+ created_at = None
1203
+ if self.include_metadata & MetadataOptions.CREATED_AT:
1204
+ created_at_str = result.get("created_at")
1205
+ created_at = Timestamp(created_at_str) if created_at_str else None
1206
+
1207
+ created_by_str = None
1208
+ if self.include_metadata & MetadataOptions.CREATED_BY:
1209
+ created_by_str = result.get("created_by")
1210
+
1211
+ updated_at = None
1212
+ if self.include_metadata & MetadataOptions.UPDATED_AT:
1213
+ updated_at_str = result.get("updated_at")
1214
+ updated_at = Timestamp(updated_at_str) if updated_at_str else None
1215
+
1216
+ updated_by_str = None
1217
+ if self.include_metadata & MetadataOptions.UPDATED_BY:
1218
+ updated_by_str = result.get("updated_by")
1219
+
940
1220
  direction_enum = {
941
1221
  "inbound": RelationshipDirection.INBOUND,
942
1222
  "outbound": RelationshipDirection.OUTBOUND,
943
1223
  "bidirectional": RelationshipDirection.BIDIR,
944
1224
  }.get(direction)
945
- gpn.add_peer(node_id=node_id, rel_name=rel_name, peer_id=peer_id, direction=direction_enum)
1225
+ gpn.add_peer(
1226
+ node_id=node_id,
1227
+ rel_name=rel_name,
1228
+ peer_id=peer_id,
1229
+ direction=direction_enum,
1230
+ created_at=created_at,
1231
+ created_by=created_by_str,
1232
+ updated_at=updated_at,
1233
+ updated_by=updated_by_str,
1234
+ )
946
1235
 
947
1236
  return gpn
948
1237
 
@@ -986,11 +1275,81 @@ class NodeListGetInfoQuery(Query):
986
1275
  name = "node_list_get_info"
987
1276
  type = QueryType.READ
988
1277
 
989
- def __init__(self, ids: list[str], account=None, **kwargs: Any) -> None:
990
- self.account = account
1278
+ def __init__(self, ids: list[str], include_metadata: MetadataOptions = MetadataOptions.NONE, **kwargs: Any) -> None:
991
1279
  self.ids = ids
1280
+ self.include_metadata = include_metadata
992
1281
  super().__init__(**kwargs)
993
1282
 
1283
+ def _needs_user_timestamp_metadata(self) -> bool:
1284
+ return bool(self.include_metadata & MetadataOptions.USER_TIMESTAMPS)
1285
+
1286
+ def _add_created_metadata_to_query(self, branch_filter_str: str) -> None:
1287
+ if self.branch.is_default or self.branch.is_global:
1288
+ created_metadata_query = """
1289
+ WITH *, n.created_at AS created_at, n.created_by AS created_by
1290
+ """
1291
+ else:
1292
+ created_metadata_query = """
1293
+ CALL (n) {
1294
+ MATCH (:Node {uuid: n.uuid})-[r:IS_PART_OF {status: "active"}]->(:Root)
1295
+ WHERE %(branch_filter)s
1296
+ RETURN r.from AS created_at, r.from_user_id AS created_by
1297
+ ORDER BY r.from ASC
1298
+ LIMIT 1
1299
+ }
1300
+ """ % {"branch_filter": branch_filter_str}
1301
+ self.add_to_query(created_metadata_query)
1302
+ self.return_labels.extend(["created_at", "created_by"])
1303
+
1304
+ def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
1305
+ if self.branch.is_default or self.branch.is_global:
1306
+ last_update_query = """
1307
+ WITH *, n.updated_at AS updated_at, n.updated_by AS updated_by
1308
+ """
1309
+ else:
1310
+ if self.branch_agnostic:
1311
+ time_details = """
1312
+ WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
1313
+ """
1314
+ else:
1315
+ time_details = """
1316
+ WITH CASE
1317
+ WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
1318
+ WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
1319
+ ELSE [NULL, NULL]
1320
+ END AS from_details,
1321
+ CASE
1322
+ WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
1323
+ WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
1324
+ ELSE [NULL, NULL]
1325
+ END AS to_details
1326
+ """
1327
+ last_update_query = """
1328
+ MATCH (n)-[r:HAS_ATTRIBUTE|IS_RELATED]-(field:Attribute|Relationship)
1329
+ WHERE %(branch_filter)s
1330
+ WITH DISTINCT n, r_is_part_of, field
1331
+ CALL (field) {
1332
+ MATCH (field)-[r]-(property)
1333
+ WHERE %(branch_filter)s
1334
+ %(time_details)s
1335
+ WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
1336
+ WITH from_details_list + to_details_list AS details_list
1337
+ UNWIND details_list AS one_details
1338
+ WITH one_details[0] AS updated_at, one_details[1] AS updated_by
1339
+ WHERE updated_at IS NOT NULL
1340
+ WITH updated_at, updated_by
1341
+ ORDER BY updated_at DESC
1342
+ LIMIT 1
1343
+ RETURN updated_at, updated_by
1344
+ }
1345
+ WITH n, r_is_part_of, updated_at, updated_by
1346
+ // updated_by ordering preferences non "__system__" users
1347
+ ORDER BY elementId(n), updated_at DESC, updated_by DESC
1348
+ WITH n, r_is_part_of, head(collect(updated_at)) AS updated_at, head(collect(updated_by)) AS updated_by
1349
+ """ % {"branch_filter": branch_filter_str, "time_details": time_details}
1350
+ self.add_to_query(last_update_query)
1351
+ self.return_labels.extend(["updated_at", "updated_by"])
1352
+
994
1353
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
995
1354
  branch_filter, branch_params = self.branch.get_query_filter_path(
996
1355
  at=self.at, branch_agnostic=self.branch_agnostic
@@ -1005,36 +1364,115 @@ class NodeListGetInfoQuery(Query):
1005
1364
  CALL (root, n) {
1006
1365
  MATCH (root:Root)<-[r:IS_PART_OF]-(n:Node)
1007
1366
  WHERE %(branch_filter)s
1008
- RETURN n as n1, r as r1
1009
- ORDER BY r.branch_level DESC, r.from DESC
1367
+ RETURN r AS r_is_part_of
1368
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
1010
1369
  LIMIT 1
1011
1370
  }
1012
- WITH n1 as n, r1 as rb
1013
- WHERE rb.status = "active"
1371
+ WITH n, r_is_part_of
1372
+ WHERE r_is_part_of.status = "active"
1014
1373
  """ % {"branch_filter": branch_filter}
1015
-
1016
1374
  self.add_to_query(query)
1375
+ self.return_labels = [
1376
+ "labels(n) AS node_labels",
1377
+ "r_is_part_of.branch AS branch",
1378
+ "elementId(n) AS node_database_id",
1379
+ "n.uuid AS node_uuid",
1380
+ ]
1017
1381
 
1018
- self.return_labels = ["n", "rb"]
1382
+ if self._needs_user_timestamp_metadata():
1383
+ self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
1384
+ self._add_created_metadata_to_query(branch_filter_str=branch_filter)
1019
1385
 
1020
1386
  async def get_nodes(self, db: InfrahubDatabase, duplicate: bool = False) -> AsyncIterator[NodeToProcess]:
1021
1387
  """Return all the node objects as NodeToProcess."""
1022
1388
 
1023
- for result in self.get_results_group_by(("n", "uuid")):
1024
- schema = find_node_schema(db=db, node=result.get_node("n"), branch=self.branch, duplicate=duplicate)
1389
+ for result in self.get_results():
1390
+ raw_labels: list[str] = result.get_as_type(label="node_labels", return_type=list)
1391
+ labels = [str(lbl) for lbl in raw_labels]
1392
+ schema = find_node_schema(db=db, branch=self.branch, labels=labels, duplicate=duplicate)
1025
1393
  node_branch = self.branch
1026
1394
  if self.branch_agnostic:
1027
- node_branch = result.get_rel("rb").get("branch")
1395
+ node_branch = result.get_as_type(label="branch", return_type=str)
1396
+
1397
+ created_at = None
1398
+ created_by = None
1399
+ if self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.UPDATED_AT):
1400
+ raw_created_at = result.get_as_str(label="created_at")
1401
+ created_at = Timestamp(raw_created_at) if raw_created_at else None
1402
+ if self.include_metadata & (MetadataOptions.CREATED_BY | MetadataOptions.UPDATED_BY):
1403
+ created_by = result.get_as_str(label="created_by")
1404
+ updated_at = None
1405
+ updated_by = None
1406
+ if self.include_metadata & MetadataOptions.UPDATED_AT:
1407
+ raw_updated_at = result.get_as_str(label="updated_at")
1408
+ updated_at = Timestamp(raw_updated_at) if raw_updated_at else None
1409
+ if self.include_metadata & MetadataOptions.UPDATED_BY:
1410
+ updated_by = result.get_as_str(label="updated_by")
1411
+
1028
1412
  yield NodeToProcess(
1029
1413
  schema=schema,
1030
- node_id=result.get_node("n").element_id,
1031
- node_uuid=result.get_node("n").get("uuid"),
1032
- updated_at=result.get_rel("rb").get("from"),
1414
+ node_id=result.get_as_type(label="node_database_id", return_type=str),
1415
+ node_uuid=result.get_as_type(label="node_uuid", return_type=str),
1033
1416
  branch=node_branch,
1034
- labels=list(result.get_node("n").labels),
1417
+ labels=labels,
1418
+ created_at=created_at,
1419
+ created_by=created_by,
1420
+ updated_at=updated_at or created_at,
1421
+ updated_by=updated_by or created_by,
1035
1422
  )
1036
1423
 
1037
1424
 
1425
+ class NodeGetByHFIDQuery(Query):
1426
+ """Query to lookup nodes by their HFID.
1427
+
1428
+ This query uses the stored `human_friendly_id` attribute on nodes.
1429
+ """
1430
+
1431
+ name = "node_get_by_hfid"
1432
+ type = QueryType.READ
1433
+
1434
+ def __init__(self, node_kind: str, hfids: list[list[str]], **kwargs: Any) -> None:
1435
+ self.node_kind = node_kind
1436
+ self.hfids = hfids
1437
+ super().__init__(**kwargs)
1438
+
1439
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
1440
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
1441
+ self.params.update(branch_params)
1442
+ # The list is stored as a string in the database
1443
+ self.params["hfid_values"] = [ujson.dumps(hfid) for hfid in self.hfids]
1444
+
1445
+ query = """
1446
+ MATCH (n:%(node_kind)s)
1447
+ CALL (n) {
1448
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
1449
+ WHERE %(branch_filter)s
1450
+ RETURN r AS r_part_of
1451
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
1452
+ LIMIT 1
1453
+ }
1454
+ WITH n, r_part_of
1455
+ WHERE r_part_of.status = "active"
1456
+ MATCH (n)-[:HAS_ATTRIBUTE]->(attr:Attribute {name: "human_friendly_id"})
1457
+ CALL (attr) {
1458
+ MATCH (attr)-[r:HAS_VALUE]->(av)
1459
+ WHERE %(branch_filter)s
1460
+ RETURN av, r AS r_attr
1461
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
1462
+ LIMIT 1
1463
+ }
1464
+ WITH n, av, r_attr
1465
+ WHERE r_attr.status = "active" AND av.value IN $hfid_values
1466
+ """ % {"branch_filter": branch_filter, "node_kind": self.node_kind}
1467
+
1468
+ self.add_to_query(query)
1469
+ self.return_labels = ["n.uuid AS node_uuid", "av.value AS hfid"]
1470
+
1471
+ def get_node_uuids(self) -> list[str]:
1472
+ """Get the list of node UUIDs from the query results."""
1473
+ return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
1474
+
1475
+
1038
1476
  class FieldAttributeRequirementType(Enum):
1039
1477
  FILTER = "filter"
1040
1478
  ORDER = "order"