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
@@ -9,18 +9,17 @@ from netaddr import IPSet
9
9
  from opentelemetry import trace
10
10
 
11
11
  from infrahub.core import registry
12
- from infrahub.core.constants import InfrahubKind
12
+ from infrahub.core.constants import InfrahubKind, MetadataOptions
13
13
  from infrahub.core.ipam.constants import PrefixMemberType
14
14
  from infrahub.core.manager import NodeManager
15
15
  from infrahub.core.node import Node
16
16
  from infrahub.core.protocols import BuiltinIPNamespace, BuiltinIPPrefix
17
17
  from infrahub.core.schema.generic_schema import GenericSchema
18
18
  from infrahub.exceptions import ValidationError
19
+ from infrahub.graphql.models import OrderModel
19
20
  from infrahub.graphql.parser import extract_selection
20
21
  from infrahub.graphql.permissions import get_permissions
21
22
 
22
- from ..models import OrderModel
23
-
24
23
  if TYPE_CHECKING:
25
24
  from collections.abc import Sequence
26
25
 
@@ -31,7 +30,6 @@ if TYPE_CHECKING:
31
30
  from infrahub.core.schema import NodeSchema
32
31
  from infrahub.database import InfrahubDatabase
33
32
  from infrahub.graphql.initialization import GraphqlContext
34
- from infrahub.graphql.models import OrderModel
35
33
 
36
34
 
37
35
  def _ip_range_display_label(node: Node) -> str:
@@ -311,7 +309,7 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
311
309
  info: GraphQLResolveInfo,
312
310
  offset: int | None = None,
313
311
  limit: int | None = None,
314
- order: OrderModel | None = None,
312
+ order: dict[str, Any] | None = None,
315
313
  partial_match: bool = False,
316
314
  **kwargs: dict[str, Any],
317
315
  ) -> dict[str, Any]:
@@ -324,6 +322,7 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
324
322
  if not isinstance(schema, GenericSchema) or schema.kind not in [InfrahubKind.IPADDRESS, InfrahubKind.IPPREFIX]:
325
323
  raise ValidationError(f"{schema.kind} is not {InfrahubKind.IPADDRESS} or {InfrahubKind.IPPREFIX}")
326
324
 
325
+ order_model = OrderModel.from_input(input_data=order)
327
326
  fields = await extract_selection(info=info, schema=schema)
328
327
  resolve_available = bool(kwargs.pop("include_available", False))
329
328
  kinds_to_filter: list[str] = kwargs.pop("kinds", []) # type: ignore[assignment]
@@ -400,10 +399,9 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
400
399
  limit=query_limit,
401
400
  offset=offset,
402
401
  account=graphql_context.account_session,
403
- include_source=True,
404
- include_owner=True,
402
+ include_metadata=MetadataOptions.LINKED_NODES,
405
403
  partial_match=partial_match,
406
- order=order,
404
+ order=order_model,
407
405
  )
408
406
 
409
407
  if fetch_first_node_context and len(objs) > 2:
@@ -3,15 +3,20 @@ from typing import TYPE_CHECKING, Any
3
3
  from graphql import GraphQLResolveInfo
4
4
 
5
5
  from infrahub.core.branch.models import Branch
6
- from infrahub.core.constants import BranchSupportType, RelationshipHierarchyDirection
6
+ from infrahub.core.constants import (
7
+ BranchSupportType,
8
+ RelationshipHierarchyDirection,
9
+ )
7
10
  from infrahub.core.manager import NodeManager
11
+ from infrahub.core.metadata.model import MetadataQueryOptions
8
12
  from infrahub.core.query.node import NodeGetHierarchyQuery
13
+ from infrahub.core.relationship import Relationship
9
14
  from infrahub.core.schema.node_schema import NodeSchema
10
15
  from infrahub.core.schema.relationship_schema import RelationshipSchema
11
16
  from infrahub.core.timestamp import Timestamp
12
17
  from infrahub.database import InfrahubDatabase
13
18
  from infrahub.graphql.field_extractor import extract_graphql_fields
14
- from infrahub.utils import has_any_key
19
+ from infrahub.graphql.metadata import build_metadata_query_options, get_metadata_options_from_fields
15
20
 
16
21
  from ..loaders.peers import PeerRelationshipsDataLoader, QueryPeerParams
17
22
  from ..types import RELATIONS_PROPERTY_MAP, RELATIONS_PROPERTY_MAP_REVERSED
@@ -68,6 +73,25 @@ class ManyRelationshipResolver:
68
73
  branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
69
74
  )
70
75
 
76
+ def _build_relationship_meta_response(
77
+ self, relationship: Relationship, metadata_fields: dict[str, Any]
78
+ ) -> dict[str, Any]:
79
+ data: dict[str, Any] = {}
80
+ for meta_field in metadata_fields.keys():
81
+ if meta_field == "created_at":
82
+ created_at = relationship._get_created_at()
83
+ data["created_at"] = created_at.to_datetime() if created_at else None
84
+ elif meta_field == "created_by":
85
+ account_id = relationship._get_created_by()
86
+ data["created_by"] = {"id": account_id} if account_id else None
87
+ elif meta_field == "updated_at":
88
+ updated_at = relationship._get_updated_at()
89
+ data["updated_at"] = updated_at.to_datetime() if updated_at else None
90
+ elif meta_field == "updated_by":
91
+ account_id = relationship._get_updated_by()
92
+ data["updated_by"] = {"id": account_id} if account_id else None
93
+ return data
94
+
71
95
  async def resolve(
72
96
  self,
73
97
  parent: dict,
@@ -93,6 +117,10 @@ class ManyRelationshipResolver:
93
117
  edges = fields.get("edges", {})
94
118
  node_fields = edges.get("node", {})
95
119
  property_fields = edges.get("properties", {})
120
+ metadata_fields = {
121
+ "node_metadata": edges.get("node_metadata", {}),
122
+ "relationship_metadata": edges.get("relationship_metadata", {}),
123
+ }
96
124
  for key, value in property_fields.items():
97
125
  mapped_name = RELATIONS_PROPERTY_MAP[key]
98
126
  node_fields[mapped_name] = value
@@ -138,45 +166,71 @@ class ManyRelationshipResolver:
138
166
  if not node_fields:
139
167
  return response
140
168
 
169
+ include_metadata = build_metadata_query_options(
170
+ node_metadata_fields=metadata_fields.get("node_metadata"),
171
+ relationship_metadata_fields=metadata_fields.get("relationship_metadata"),
172
+ node_fields=node_fields,
173
+ )
174
+ # Add relationship properties metadata to relationship_level
175
+ include_metadata |= MetadataQueryOptions(relationship_level=get_metadata_options_from_fields(property_fields))
176
+
141
177
  if offset or limit:
142
- node_graph = await self._get_entities_simple(
178
+ relationships = await self._get_entities_simple(
143
179
  db=graphql_context.db,
144
180
  branch=graphql_context.branch,
145
181
  ids=ids,
146
182
  at=graphql_context.at,
147
- related_node_ids=graphql_context.related_node_ids,
148
183
  source_kind=source_kind,
149
184
  rel_schema=node_rel,
150
185
  filters=filters,
151
186
  node_fields=node_fields,
187
+ include_metadata=include_metadata,
152
188
  offset=offset,
153
189
  limit=limit,
154
190
  )
155
191
  else:
156
- node_graph = await self._get_entities_with_data_loader(
192
+ relationships = await self._get_entities_with_data_loader(
157
193
  db=graphql_context.db,
158
194
  branch=graphql_context.branch,
159
195
  ids=ids,
160
196
  at=graphql_context.at,
161
- related_node_ids=graphql_context.related_node_ids,
162
197
  source_kind=source_kind,
163
198
  rel_schema=node_rel,
164
199
  filters=filters,
165
200
  node_fields=node_fields,
201
+ include_metadata=include_metadata,
166
202
  )
167
203
 
168
- if not node_graph:
204
+ if not relationships:
169
205
  return response
170
206
 
171
207
  entries = []
172
- for node in node_graph:
173
- entry: dict[str, dict[str, Any]] = {"node": {}, "properties": {}}
174
- for key, mapped in RELATIONS_PROPERTY_MAP_REVERSED.items():
175
- value = node.pop(key, None)
176
- if value:
177
- entry["properties"][mapped] = value
178
- entry["node"] = node
179
- entries.append(entry)
208
+ async with graphql_context.db.start_session(read_only=True) as db:
209
+ for rel in relationships:
210
+ node = await rel.to_graphql(
211
+ db=db,
212
+ fields=node_fields,
213
+ related_node_ids=graphql_context.related_node_ids,
214
+ )
215
+ entry: dict[str, dict[str, Any]] = {"node": {}, "properties": {}}
216
+ for key, mapped in RELATIONS_PROPERTY_MAP_REVERSED.items():
217
+ value = node.pop(key, None)
218
+ if value:
219
+ entry["properties"][mapped] = value
220
+ entry["node"] = node
221
+
222
+ if metadata_fields.get("node_metadata"):
223
+ peer = await rel.get_peer(db=db)
224
+ if peer:
225
+ entry["node_metadata"] = await peer._build_meta_response("node_metadata", edges)
226
+
227
+ if metadata_fields.get("relationship_metadata"):
228
+ entry["relationship_metadata"] = self._build_relationship_meta_response(
229
+ relationship=rel,
230
+ metadata_fields=metadata_fields["relationship_metadata"],
231
+ )
232
+
233
+ entries.append(entry)
180
234
 
181
235
  response["edges"] = entries
182
236
  return response
@@ -187,17 +241,14 @@ class ManyRelationshipResolver:
187
241
  branch: Branch,
188
242
  ids: list[str],
189
243
  at: Timestamp | None,
190
- related_node_ids: set[str] | None,
191
244
  source_kind: str,
192
245
  rel_schema: RelationshipSchema,
193
246
  filters: dict[str, Any],
194
247
  node_fields: dict[str, Any],
248
+ include_metadata: MetadataQueryOptions,
195
249
  offset: int | None = None,
196
250
  limit: int | None = None,
197
- ) -> list[dict[str, Any]] | None:
198
- include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
199
- include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
200
-
251
+ ) -> list[Relationship] | None:
201
252
  async with db.start_session(read_only=True) as dbs:
202
253
  objs = await NodeManager.query_peers(
203
254
  db=dbs,
@@ -212,12 +263,11 @@ class ManyRelationshipResolver:
212
263
  branch=branch,
213
264
  branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
214
265
  fetch_peers=True,
215
- include_source=include_source,
216
- include_owner=include_owner,
266
+ include_metadata=include_metadata,
217
267
  )
218
268
  if not objs:
219
269
  return None
220
- return [await obj.to_graphql(db=dbs, fields=node_fields, related_node_ids=related_node_ids) for obj in objs]
270
+ return objs
221
271
 
222
272
  async def _get_entities_with_data_loader(
223
273
  self,
@@ -225,18 +275,15 @@ class ManyRelationshipResolver:
225
275
  branch: Branch,
226
276
  ids: list[str],
227
277
  at: Timestamp | None,
228
- related_node_ids: set[str] | None,
229
278
  source_kind: str,
230
279
  rel_schema: RelationshipSchema,
231
280
  filters: dict[str, Any],
232
281
  node_fields: dict[str, Any],
233
- ) -> list[dict[str, Any]] | None:
282
+ include_metadata: MetadataQueryOptions,
283
+ ) -> list[Relationship] | None:
234
284
  if node_fields and "hfid" in node_fields:
235
285
  node_fields["human_friendly_id"] = None
236
286
 
237
- include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
238
- include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
239
-
240
287
  query_params = QueryPeerParams(
241
288
  branch=branch,
242
289
  source_kind=source_kind,
@@ -245,8 +292,7 @@ class ManyRelationshipResolver:
245
292
  fields=node_fields,
246
293
  at=at,
247
294
  branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
248
- include_source=include_source,
249
- include_owner=include_owner,
295
+ include_metadata=include_metadata,
250
296
  )
251
297
  if query_params in self._data_loader_instances:
252
298
  loader = self._data_loader_instances[query_params]
@@ -259,8 +305,4 @@ class ManyRelationshipResolver:
259
305
  all_peer_rels.extend(node_peer_rels)
260
306
  if not all_peer_rels:
261
307
  return None
262
- async with db.start_session(read_only=True) as dbs:
263
- return [
264
- await obj.to_graphql(db=dbs, fields=node_fields, related_node_ids=related_node_ids)
265
- for obj in all_peer_rels
266
- ]
308
+ return all_peer_rels
@@ -9,7 +9,7 @@ from infrahub.core.constants import BranchSupportType, InfrahubKind, Relationshi
9
9
  from infrahub.core.manager import NodeManager
10
10
  from infrahub.exceptions import NodeNotFoundError
11
11
  from infrahub.graphql.field_extractor import extract_graphql_fields
12
- from infrahub.utils import has_any_key
12
+ from infrahub.graphql.metadata import build_metadata_query_options
13
13
 
14
14
  from ..models import OrderModel
15
15
  from ..parser import extract_selection
@@ -18,7 +18,7 @@ from ..permissions import get_permissions
18
18
  if TYPE_CHECKING:
19
19
  from graphql import GraphQLResolveInfo
20
20
 
21
- from infrahub.core.schema import NodeSchema
21
+ from infrahub.core.schema import MainSchemaTypes, NodeSchema
22
22
  from infrahub.graphql.initialization import GraphqlContext
23
23
 
24
24
 
@@ -146,16 +146,18 @@ async def default_paginated_list_resolver(
146
146
  info: GraphQLResolveInfo,
147
147
  offset: int | None = None,
148
148
  limit: int | None = None,
149
- order: OrderModel | None = None,
149
+ order: dict | None = None,
150
150
  partial_match: bool = False,
151
151
  **kwargs: dict[str, Any],
152
152
  ) -> dict[str, Any]:
153
- schema: NodeSchema = (
153
+ schema: MainSchemaTypes = (
154
154
  info.return_type.of_type.graphene_type._meta.schema
155
155
  if isinstance(info.return_type, GraphQLNonNull)
156
156
  else info.return_type.graphene_type._meta.schema
157
157
  )
158
158
 
159
+ order_model = OrderModel.from_input(input_data=order)
160
+
159
161
  fields = await extract_selection(info=info, schema=schema)
160
162
 
161
163
  graphql_context: GraphqlContext = info.context
@@ -165,8 +167,13 @@ async def default_paginated_list_resolver(
165
167
  key: value for key, value in kwargs.items() if ("__" in key and value is not None) or key in ("ids", "hfid")
166
168
  }
167
169
 
168
- edges = fields.get("edges", {})
170
+ edges: dict[str, Any] = fields.get("edges", {})
169
171
  node_fields = edges.get("node", {})
172
+ node_metadata_fields: dict[str, Any] = edges.get("node_metadata", {})
173
+ include_metadata = build_metadata_query_options(
174
+ node_metadata_fields=node_metadata_fields,
175
+ node_fields=node_fields,
176
+ )
170
177
  if "hfid" in node_fields:
171
178
  node_fields["human_friendly_id"] = None
172
179
 
@@ -186,23 +193,19 @@ async def default_paginated_list_resolver(
186
193
 
187
194
  objs = []
188
195
  if edges or "hfid" in filters:
189
- include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
190
- include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
191
-
192
196
  objs = await NodeManager.query(
193
197
  db=db,
194
198
  schema=schema,
195
199
  filters=filters or None,
196
200
  fields=node_fields,
201
+ include_metadata=include_metadata,
197
202
  at=graphql_context.at,
198
203
  branch=graphql_context.branch,
199
204
  limit=limit,
200
205
  offset=offset,
201
206
  account=graphql_context.account_session,
202
- include_source=include_source,
203
- include_owner=include_owner,
204
207
  partial_match=partial_match,
205
- order=order,
208
+ order=order_model,
206
209
  )
207
210
 
208
211
  if "count" in fields:
@@ -227,7 +230,8 @@ async def default_paginated_list_resolver(
227
230
  fields=node_fields,
228
231
  related_node_ids=graphql_context.related_node_ids,
229
232
  permissions=permission_set,
230
- )
233
+ ),
234
+ "node_metadata": await obj._build_meta_response("node_metadata", edges),
231
235
  }
232
236
  for obj in objs
233
237
  ]
@@ -4,12 +4,16 @@ from graphql import GraphQLResolveInfo
4
4
  from graphql.type.definition import GraphQLNonNull
5
5
 
6
6
  from infrahub.core.branch.models import Branch
7
- from infrahub.core.constants import BranchSupportType
7
+ from infrahub.core.constants import BranchSupportType, MetadataOptions
8
8
  from infrahub.core.manager import NodeManager
9
+ from infrahub.core.metadata.model import MetadataQueryOptions
10
+ from infrahub.core.node import Node
11
+ from infrahub.core.relationship import Relationship
9
12
  from infrahub.core.schema.relationship_schema import RelationshipSchema
10
13
  from infrahub.core.timestamp import Timestamp
11
14
  from infrahub.database import InfrahubDatabase
12
15
  from infrahub.graphql.field_extractor import extract_graphql_fields
16
+ from infrahub.graphql.metadata import build_metadata_query_options, get_metadata_options_from_fields
13
17
 
14
18
  from ..loaders.node import GetManyParams, NodeDataLoader
15
19
  from ..types import RELATIONS_PROPERTY_MAP, RELATIONS_PROPERTY_MAP_REVERSED
@@ -24,6 +28,25 @@ class SingleRelationshipResolver:
24
28
  def __init__(self) -> None:
25
29
  self._data_loader_instances: dict[GetManyParams, NodeDataLoader] = {}
26
30
 
31
+ def _build_relationship_meta_response(
32
+ self, relationship: Relationship, metadata_fields: dict[str, Any]
33
+ ) -> dict[str, Any]:
34
+ data: dict[str, Any] = {}
35
+ for meta_field in metadata_fields.keys():
36
+ if meta_field == "created_at":
37
+ created_at = relationship._get_created_at()
38
+ data["created_at"] = created_at.to_datetime() if created_at else None
39
+ elif meta_field == "created_by":
40
+ account_id = relationship._get_created_by()
41
+ data["created_by"] = {"id": account_id} if account_id else None
42
+ elif meta_field == "updated_at":
43
+ updated_at = relationship._get_updated_at()
44
+ data["updated_at"] = updated_at.to_datetime() if updated_at else None
45
+ elif meta_field == "updated_by":
46
+ account_id = relationship._get_updated_by()
47
+ data["updated_by"] = {"id": account_id} if account_id else None
48
+ return data
49
+
27
50
  async def resolve(self, parent: dict, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]:
28
51
  """Resolver for relationships of cardinality=one for Edged responses
29
52
 
@@ -45,50 +68,92 @@ class SingleRelationshipResolver:
45
68
  fields = extract_graphql_fields(info=info)
46
69
  node_fields = fields.get("node", {})
47
70
  property_fields = fields.get("properties", {})
71
+ metadata_fields = {
72
+ "node_metadata": fields.get("node_metadata", {}),
73
+ "relationship_metadata": fields.get("relationship_metadata", {}),
74
+ }
48
75
  for key, value in property_fields.items():
49
76
  mapped_name = RELATIONS_PROPERTY_MAP[key]
50
77
  node_fields[mapped_name] = value
51
78
 
52
79
  metadata_field_names = {prop_name for prop_name in RELATIONS_PROPERTY_MAP if prop_name != "__typename"}
53
- requires_relationship_metadata = bool(set(property_fields.keys()) & metadata_field_names)
80
+ requires_relationship_properties = bool(set(property_fields.keys()) & metadata_field_names)
81
+ requires_relationship_metadata = bool(metadata_fields["relationship_metadata"])
54
82
 
55
83
  # Extract the schema of the node on the other end of the relationship from the GQL Schema
56
84
  node_rel = node_schema.get_relationship(info.field_name)
57
85
 
58
86
  response: dict[str, Any] = {"node": None, "properties": {}}
59
87
 
60
- if requires_relationship_metadata:
61
- node_graph = await self._get_entities_simple(
88
+ relationship: Relationship | None = None
89
+ peer_node: Node | None = None
90
+
91
+ if requires_relationship_properties or requires_relationship_metadata:
92
+ include_metadata = build_metadata_query_options(
93
+ node_metadata_fields=metadata_fields.get("node_metadata"),
94
+ relationship_metadata_fields=metadata_fields.get("relationship_metadata"),
95
+ node_fields=node_fields,
96
+ )
97
+ # Add relationship properties metadata to relationship_level
98
+ include_metadata |= MetadataQueryOptions(
99
+ relationship_level=get_metadata_options_from_fields(property_fields)
100
+ )
101
+ relationship = await self._get_entities_simple(
62
102
  db=graphql_context.db,
63
103
  branch=graphql_context.branch,
64
104
  at=graphql_context.at,
65
- related_node_ids=graphql_context.related_node_ids,
66
105
  field_name=info.field_name,
67
106
  parent_id=parent["id"],
68
107
  source_kind=node_schema.kind,
69
108
  rel_schema=node_rel,
70
109
  node_fields=node_fields,
110
+ include_metadata=include_metadata,
71
111
  **kwargs,
72
112
  )
73
113
  else:
74
- node_graph = await self._get_entities_with_data_loader(
114
+ include_metadata = build_metadata_query_options(
115
+ node_metadata_fields=metadata_fields.get("node_metadata"),
116
+ node_fields=node_fields,
117
+ )
118
+ peer_node = await self._get_entities_with_data_loader(
75
119
  db=graphql_context.db,
76
120
  branch=graphql_context.branch,
77
121
  at=graphql_context.at,
78
- related_node_ids=graphql_context.related_node_ids,
79
122
  rel_schema=node_rel,
80
123
  parent=parent,
81
124
  node_fields=node_fields,
125
+ include_metadata=include_metadata,
82
126
  )
83
127
 
84
- if not node_graph:
128
+ if not relationship and not peer_node:
85
129
  return response
86
- response["node"] = node_graph
87
130
 
88
- for key, mapped in RELATIONS_PROPERTY_MAP_REVERSED.items():
89
- value = node_graph.pop(key, None)
90
- if value:
91
- response["properties"][mapped] = value
131
+ async with graphql_context.db.start_session(read_only=True) as db:
132
+ if relationship:
133
+ node_graph = await relationship.to_graphql(
134
+ db=db, fields=node_fields, related_node_ids=graphql_context.related_node_ids
135
+ )
136
+ peer_node = await relationship.get_peer(db=db)
137
+ elif peer_node:
138
+ node_graph = await peer_node.to_graphql(
139
+ db=db, fields=node_fields, related_node_ids=graphql_context.related_node_ids
140
+ )
141
+
142
+ response["node"] = node_graph
143
+
144
+ for key, mapped in RELATIONS_PROPERTY_MAP_REVERSED.items():
145
+ value = node_graph.pop(key, None)
146
+ if value:
147
+ response["properties"][mapped] = value
148
+
149
+ if metadata_fields.get("node_metadata") and peer_node:
150
+ response["node_metadata"] = await peer_node._build_meta_response("node_metadata", fields)
151
+
152
+ if metadata_fields.get("relationship_metadata") and relationship:
153
+ response["relationship_metadata"] = self._build_relationship_meta_response(
154
+ relationship=relationship, metadata_fields=metadata_fields["relationship_metadata"]
155
+ )
156
+
92
157
  return response
93
158
 
94
159
  async def _get_entities_simple(
@@ -96,14 +161,14 @@ class SingleRelationshipResolver:
96
161
  db: InfrahubDatabase,
97
162
  branch: Branch,
98
163
  at: Timestamp | None,
99
- related_node_ids: set[str] | None,
100
164
  field_name: str,
101
165
  parent_id: str,
102
166
  source_kind: str,
103
167
  rel_schema: RelationshipSchema,
104
168
  node_fields: dict[str, Any],
169
+ include_metadata: MetadataQueryOptions,
105
170
  **kwargs: Any,
106
- ) -> dict[str, Any] | None:
171
+ ) -> Relationship | None:
107
172
  filters = {
108
173
  f"{field_name}__{key}": value
109
174
  for key, value in kwargs.items()
@@ -121,21 +186,22 @@ class SingleRelationshipResolver:
121
186
  branch=branch,
122
187
  branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
123
188
  fetch_peers=True,
189
+ include_metadata=include_metadata,
124
190
  )
125
191
  if not objs:
126
192
  return None
127
- return await objs[0].to_graphql(db=dbs, fields=node_fields, related_node_ids=related_node_ids)
193
+ return objs[0]
128
194
 
129
195
  async def _get_entities_with_data_loader(
130
196
  self,
131
197
  db: InfrahubDatabase,
132
198
  branch: Branch,
133
199
  at: Timestamp | None,
134
- related_node_ids: set[str] | None,
135
200
  rel_schema: RelationshipSchema,
136
201
  parent: dict[str, Any],
137
202
  node_fields: dict[str, Any],
138
- ) -> dict[str, Any] | None:
203
+ include_metadata: MetadataQueryOptions,
204
+ ) -> Node | None:
139
205
  try:
140
206
  peer_id: str = parent[rel_schema.name][0]["node"]["id"]
141
207
  except (KeyError, IndexError):
@@ -144,14 +210,13 @@ class SingleRelationshipResolver:
144
210
  if node_fields and "hfid" in node_fields:
145
211
  node_fields["human_friendly_id"] = None
146
212
 
213
+ include_metadata |= MetadataQueryOptions(attribute_level=MetadataOptions.LINKED_NODES)
147
214
  query_params = GetManyParams(
148
215
  fields=node_fields,
149
216
  at=at,
150
217
  branch=branch,
151
- include_source=True,
152
- include_owner=True,
218
+ include_metadata=include_metadata,
153
219
  prefetch_relationships=False,
154
- account=None,
155
220
  branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
156
221
  )
157
222
  if query_params in self._data_loader_instances:
@@ -162,5 +227,4 @@ class SingleRelationshipResolver:
162
227
  node = await loader.load(key=peer_id)
163
228
  if not node:
164
229
  return None
165
- async with db.start_session(read_only=True) as dbs:
166
- return await node.to_graphql(db=dbs, fields=node_fields, related_node_ids=related_node_ids)
230
+ return node
@@ -8,6 +8,7 @@ from infrahub.core.constants import InfrahubKind
8
8
  from infrahub.core.manager import NodeManager
9
9
  from infrahub.core.protocols import CoreGraphQLQuery
10
10
  from infrahub.core.timestamp import Timestamp
11
+ from infrahub.graphql.resolvers.account_metadata import AccountMetadataResolver
11
12
  from infrahub.graphql.resolvers.many_relationship import ManyRelationshipResolver
12
13
  from infrahub.graphql.resolvers.single_relationship import SingleRelationshipResolver
13
14
  from infrahub.log import get_logger
@@ -50,6 +51,7 @@ async def resolver_graphql_query(
50
51
  types=graphql_context.types,
51
52
  single_relationship_resolver=SingleRelationshipResolver(),
52
53
  many_relationship_resolver=ManyRelationshipResolver(),
54
+ account_metadata_resolver=AccountMetadataResolver(),
53
55
  ),
54
56
  root_value=None,
55
57
  variable_values=params or {},
@@ -66,7 +66,6 @@ __all__ = [
66
66
 
67
67
 
68
68
  RELATIONS_PROPERTY_MAP: dict[str, str] = {
69
- "is_visible": "_relation__is_visible",
70
69
  "is_protected": "_relation__is_protected",
71
70
  "owner": "_relation__owner",
72
71
  "source": "_relation__source",
@@ -22,7 +22,6 @@ class RelatedNodeInput(InputObjectType):
22
22
  hfid = Field(List(of_type=String), required=False)
23
23
  kind = String(required=False) # Only used to resolve hfid of a related node on a generic relationship, see #4649
24
24
  from_pool = Field(GenericPoolInput, required=False)
25
- _relation__is_visible = Boolean(required=False)
26
25
  _relation__is_protected = Boolean(required=False)
27
26
  _relation__owner = String(required=False)
28
27
  _relation__source = String(required=False)
@@ -41,7 +40,6 @@ class IPPrefixPoolInput(GenericPoolInput):
41
40
  class RelatedIPAddressNodeInput(InputObjectType):
42
41
  id = String(required=False)
43
42
  from_pool = Field(IPAddressPoolInput, required=False)
44
- _relation__is_visible = Boolean(required=False)
45
43
  _relation__is_protected = Boolean(required=False)
46
44
  _relation__owner = String(required=False)
47
45
  _relation__source = String(required=False)
@@ -51,7 +49,6 @@ class RelatedIPPrefixNodeInput(InputObjectType):
51
49
  id = String(required=False)
52
50
  hfid = Field(List(of_type=String), required=False)
53
51
  from_pool = Field(IPPrefixPoolInput, required=False)
54
- _relation__is_visible = Boolean(required=False)
55
52
  _relation__is_protected = Boolean(required=False)
56
53
  _relation__owner = String(required=False)
57
54
  _relation__source = String(required=False)
@@ -65,7 +62,6 @@ class AttributeInterface(InfrahubInterface):
65
62
  is_default = Field(Boolean)
66
63
  is_inherited = Field(Boolean)
67
64
  is_protected = Field(Boolean)
68
- is_visible = Field(Boolean)
69
65
  updated_at = Field(DateTime)
70
66
  # Since source and owner are using a Type that is generated dynamically
71
67
  # these 2 fields will be dynamically inserted when we generate the GraphQL Schema
@@ -73,7 +69,16 @@ class AttributeInterface(InfrahubInterface):
73
69
  # owner = Field("DataOwner")
74
70
 
75
71
 
76
- class BaseAttribute(ObjectType):
72
+ class InfrahubAttributeMetaObject(ObjectType):
73
+ # updated_by is dynamically added in GraphQLSchemaManager.generate_object_types()
74
+ # to use the account_type (CoreGenericAccount) instead of a plain String
75
+ updated_at = DateTime(
76
+ required=False,
77
+ description="Date/Time when the attribute was last modified by a user or a system task",
78
+ )
79
+
80
+
81
+ class BaseAttribute(InfrahubAttributeMetaObject):
77
82
  id = Field(String)
78
83
  is_from_profile = Field(Boolean)
79
84
  permissions = Field(PermissionType, required=False)