infrahub-server 1.6.3__py3-none-any.whl → 1.7.0__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 (250) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/exceptions.py +2 -2
  3. infrahub/api/schema.py +3 -1
  4. infrahub/artifacts/tasks.py +1 -0
  5. infrahub/auth.py +2 -2
  6. infrahub/cli/db.py +54 -28
  7. infrahub/computed_attribute/gather.py +3 -4
  8. infrahub/computed_attribute/tasks.py +23 -6
  9. infrahub/config.py +8 -0
  10. infrahub/constants/enums.py +12 -0
  11. infrahub/core/account.py +4 -4
  12. infrahub/core/attribute.py +106 -108
  13. infrahub/core/branch/models.py +44 -71
  14. infrahub/core/branch/tasks.py +5 -3
  15. infrahub/core/changelog/diff.py +1 -20
  16. infrahub/core/changelog/models.py +0 -7
  17. infrahub/core/constants/__init__.py +17 -0
  18. infrahub/core/constants/database.py +0 -1
  19. infrahub/core/constants/schema.py +0 -1
  20. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  21. infrahub/core/diff/branch_differ.py +1 -1
  22. infrahub/core/diff/conflict_transferer.py +1 -1
  23. infrahub/core/diff/data_check_synchronizer.py +4 -3
  24. infrahub/core/diff/enricher/cardinality_one.py +2 -2
  25. infrahub/core/diff/enricher/hierarchy.py +1 -1
  26. infrahub/core/diff/enricher/labels.py +1 -1
  27. infrahub/core/diff/merger/merger.py +28 -2
  28. infrahub/core/diff/merger/serializer.py +3 -10
  29. infrahub/core/diff/model/diff.py +1 -1
  30. infrahub/core/diff/query/merge.py +376 -135
  31. infrahub/core/diff/repository/repository.py +3 -1
  32. infrahub/core/graph/__init__.py +1 -1
  33. infrahub/core/graph/constraints.py +3 -3
  34. infrahub/core/graph/schema.py +2 -12
  35. infrahub/core/ipam/reconciler.py +8 -6
  36. infrahub/core/ipam/utilization.py +8 -15
  37. infrahub/core/manager.py +133 -152
  38. infrahub/core/merge.py +1 -1
  39. infrahub/core/metadata/__init__.py +0 -0
  40. infrahub/core/metadata/interface.py +37 -0
  41. infrahub/core/metadata/model.py +31 -0
  42. infrahub/core/metadata/query/__init__.py +0 -0
  43. infrahub/core/metadata/query/node_metadata.py +301 -0
  44. infrahub/core/migrations/graph/__init__.py +4 -0
  45. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
  46. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
  47. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  48. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  49. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  50. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  51. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  52. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  53. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  54. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  55. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  56. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
  57. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
  58. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  59. infrahub/core/migrations/query/__init__.py +2 -2
  60. infrahub/core/migrations/query/attribute_add.py +17 -6
  61. infrahub/core/migrations/query/attribute_remove.py +19 -5
  62. infrahub/core/migrations/query/attribute_rename.py +21 -5
  63. infrahub/core/migrations/query/node_duplicate.py +19 -4
  64. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  65. infrahub/core/migrations/schema/attribute_kind_update.py +21 -2
  66. infrahub/core/migrations/schema/attribute_name_update.py +1 -1
  67. infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
  68. infrahub/core/migrations/schema/models.py +3 -0
  69. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  70. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  71. infrahub/core/migrations/schema/node_kind_update.py +1 -1
  72. infrahub/core/migrations/schema/node_remove.py +24 -2
  73. infrahub/core/migrations/schema/tasks.py +4 -1
  74. infrahub/core/migrations/shared.py +13 -6
  75. infrahub/core/models.py +6 -6
  76. infrahub/core/node/__init__.py +157 -58
  77. infrahub/core/node/base.py +9 -5
  78. infrahub/core/node/create.py +7 -3
  79. infrahub/core/node/delete_validator.py +1 -1
  80. infrahub/core/node/standard.py +100 -14
  81. infrahub/core/order.py +30 -0
  82. infrahub/core/property.py +0 -1
  83. infrahub/core/protocols.py +1 -0
  84. infrahub/core/protocols_base.py +10 -2
  85. infrahub/core/query/__init__.py +5 -3
  86. infrahub/core/query/attribute.py +164 -49
  87. infrahub/core/query/branch.py +58 -70
  88. infrahub/core/query/delete.py +1 -1
  89. infrahub/core/query/diff.py +7 -7
  90. infrahub/core/query/ipam.py +104 -43
  91. infrahub/core/query/node.py +1072 -281
  92. infrahub/core/query/relationship.py +531 -325
  93. infrahub/core/query/resource_manager.py +107 -18
  94. infrahub/core/query/standard_node.py +25 -5
  95. infrahub/core/query/utils.py +2 -4
  96. infrahub/core/relationship/constraints/count.py +1 -1
  97. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  98. infrahub/core/relationship/constraints/peer_parent.py +1 -1
  99. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  100. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  101. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  102. infrahub/core/relationship/model.py +293 -139
  103. infrahub/core/schema/attribute_schema.py +2 -2
  104. infrahub/core/schema/basenode_schema.py +3 -0
  105. infrahub/core/schema/definitions/core/__init__.py +8 -2
  106. infrahub/core/schema/definitions/core/account.py +10 -10
  107. infrahub/core/schema/definitions/core/artifact.py +14 -8
  108. infrahub/core/schema/definitions/core/check.py +10 -4
  109. infrahub/core/schema/definitions/core/generator.py +26 -6
  110. infrahub/core/schema/definitions/core/graphql_query.py +1 -1
  111. infrahub/core/schema/definitions/core/group.py +9 -2
  112. infrahub/core/schema/definitions/core/ipam.py +80 -10
  113. infrahub/core/schema/definitions/core/menu.py +41 -7
  114. infrahub/core/schema/definitions/core/permission.py +16 -2
  115. infrahub/core/schema/definitions/core/profile.py +16 -2
  116. infrahub/core/schema/definitions/core/propose_change.py +24 -4
  117. infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
  118. infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
  119. infrahub/core/schema/definitions/core/repository.py +10 -0
  120. infrahub/core/schema/definitions/core/resource_pool.py +8 -1
  121. infrahub/core/schema/definitions/core/template.py +19 -2
  122. infrahub/core/schema/definitions/core/transform.py +11 -5
  123. infrahub/core/schema/definitions/core/webhook.py +27 -9
  124. infrahub/core/schema/manager.py +63 -43
  125. infrahub/core/schema/relationship_schema.py +6 -2
  126. infrahub/core/schema/schema_branch.py +48 -10
  127. infrahub/core/task/task.py +4 -2
  128. infrahub/core/utils.py +3 -25
  129. infrahub/core/validators/aggregated_checker.py +1 -1
  130. infrahub/core/validators/attribute/choices.py +1 -1
  131. infrahub/core/validators/attribute/enum.py +1 -1
  132. infrahub/core/validators/attribute/kind.py +1 -1
  133. infrahub/core/validators/attribute/length.py +1 -1
  134. infrahub/core/validators/attribute/min_max.py +1 -1
  135. infrahub/core/validators/attribute/number_pool.py +1 -1
  136. infrahub/core/validators/attribute/optional.py +1 -1
  137. infrahub/core/validators/attribute/regex.py +1 -1
  138. infrahub/core/validators/determiner.py +3 -3
  139. infrahub/core/validators/node/attribute.py +1 -1
  140. infrahub/core/validators/node/relationship.py +1 -1
  141. infrahub/core/validators/relationship/peer.py +1 -1
  142. infrahub/database/__init__.py +4 -4
  143. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  144. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  145. infrahub/dependencies/registry.py +2 -0
  146. infrahub/display_labels/tasks.py +12 -3
  147. infrahub/git/integrator.py +18 -18
  148. infrahub/git/tasks.py +1 -1
  149. infrahub/git/utils.py +1 -1
  150. infrahub/graphql/constants.py +3 -0
  151. infrahub/graphql/context.py +1 -1
  152. infrahub/graphql/field_extractor.py +1 -1
  153. infrahub/graphql/initialization.py +11 -0
  154. infrahub/graphql/loaders/account.py +134 -0
  155. infrahub/graphql/loaders/node.py +5 -12
  156. infrahub/graphql/loaders/peers.py +5 -7
  157. infrahub/graphql/manager.py +175 -21
  158. infrahub/graphql/metadata.py +91 -0
  159. infrahub/graphql/mutations/account.py +6 -6
  160. infrahub/graphql/mutations/attribute.py +0 -2
  161. infrahub/graphql/mutations/branch.py +9 -5
  162. infrahub/graphql/mutations/computed_attribute.py +1 -1
  163. infrahub/graphql/mutations/display_label.py +1 -1
  164. infrahub/graphql/mutations/hfid.py +1 -1
  165. infrahub/graphql/mutations/ipam.py +4 -6
  166. infrahub/graphql/mutations/main.py +9 -4
  167. infrahub/graphql/mutations/profile.py +16 -22
  168. infrahub/graphql/mutations/proposed_change.py +4 -4
  169. infrahub/graphql/mutations/relationship.py +40 -10
  170. infrahub/graphql/mutations/repository.py +14 -12
  171. infrahub/graphql/mutations/schema.py +2 -2
  172. infrahub/graphql/order.py +14 -0
  173. infrahub/graphql/queries/branch.py +62 -6
  174. infrahub/graphql/queries/resource_manager.py +25 -24
  175. infrahub/graphql/resolvers/account_metadata.py +84 -0
  176. infrahub/graphql/resolvers/ipam.py +6 -8
  177. infrahub/graphql/resolvers/many_relationship.py +77 -35
  178. infrahub/graphql/resolvers/resolver.py +59 -14
  179. infrahub/graphql/resolvers/single_relationship.py +87 -23
  180. infrahub/graphql/subscription/graphql_query.py +2 -0
  181. infrahub/graphql/types/__init__.py +0 -1
  182. infrahub/graphql/types/attribute.py +10 -5
  183. infrahub/graphql/types/branch.py +40 -53
  184. infrahub/graphql/types/enums.py +3 -0
  185. infrahub/graphql/types/metadata.py +28 -0
  186. infrahub/graphql/types/node.py +22 -2
  187. infrahub/graphql/types/relationship.py +10 -2
  188. infrahub/graphql/types/standard_node.py +12 -7
  189. infrahub/hfid/tasks.py +12 -3
  190. infrahub/lock.py +7 -0
  191. infrahub/menu/repository.py +1 -1
  192. infrahub/patch/queries/base.py +1 -1
  193. infrahub/pools/number.py +1 -8
  194. infrahub/profiles/gather.py +56 -0
  195. infrahub/profiles/mandatory_fields_checker.py +116 -0
  196. infrahub/profiles/models.py +66 -0
  197. infrahub/profiles/node_applier.py +154 -13
  198. infrahub/profiles/queries/get_profile_data.py +143 -31
  199. infrahub/profiles/tasks.py +79 -27
  200. infrahub/profiles/triggers.py +22 -0
  201. infrahub/proposed_change/action_checker.py +1 -1
  202. infrahub/proposed_change/tasks.py +4 -1
  203. infrahub/services/__init__.py +1 -1
  204. infrahub/services/adapters/cache/nats.py +1 -1
  205. infrahub/services/adapters/cache/redis.py +7 -0
  206. infrahub/tasks/artifact.py +1 -0
  207. infrahub/transformations/tasks.py +2 -2
  208. infrahub/trigger/catalogue.py +2 -0
  209. infrahub/trigger/models.py +1 -0
  210. infrahub/trigger/setup.py +3 -3
  211. infrahub/trigger/tasks.py +3 -0
  212. infrahub/validators/tasks.py +1 -0
  213. infrahub/webhook/gather.py +1 -1
  214. infrahub/webhook/models.py +1 -1
  215. infrahub/webhook/tasks.py +23 -7
  216. infrahub/workers/dependencies.py +9 -3
  217. infrahub/workers/infrahub_async.py +13 -4
  218. infrahub/workflows/catalogue.py +19 -0
  219. infrahub_sdk/analyzer.py +2 -2
  220. infrahub_sdk/branch.py +12 -39
  221. infrahub_sdk/checks.py +4 -4
  222. infrahub_sdk/client.py +36 -0
  223. infrahub_sdk/ctl/cli_commands.py +2 -1
  224. infrahub_sdk/ctl/graphql.py +15 -4
  225. infrahub_sdk/ctl/utils.py +2 -2
  226. infrahub_sdk/enums.py +6 -0
  227. infrahub_sdk/graphql/renderers.py +21 -0
  228. infrahub_sdk/graphql/utils.py +85 -0
  229. infrahub_sdk/node/attribute.py +12 -2
  230. infrahub_sdk/node/constants.py +12 -0
  231. infrahub_sdk/node/metadata.py +69 -0
  232. infrahub_sdk/node/node.py +65 -14
  233. infrahub_sdk/node/property.py +3 -0
  234. infrahub_sdk/node/related_node.py +37 -5
  235. infrahub_sdk/node/relationship.py +18 -1
  236. infrahub_sdk/operation.py +2 -2
  237. infrahub_sdk/schema/repository.py +1 -2
  238. infrahub_sdk/transforms.py +2 -2
  239. infrahub_sdk/types.py +18 -2
  240. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
  241. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +249 -228
  242. infrahub_testcontainers/container.py +3 -3
  243. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  244. infrahub_testcontainers/docker-compose.test.yml +13 -5
  245. infrahub_testcontainers/models.py +3 -3
  246. infrahub_testcontainers/performance_test.py +1 -1
  247. infrahub/graphql/models.py +0 -6
  248. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
  249. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
  250. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -21,8 +21,10 @@ from infrahub.core.schema import (
21
21
  from infrahub.graphql.mutations.attribute import BaseAttributeCreate, BaseAttributeUpdate
22
22
  from infrahub.graphql.mutations.graphql_query import InfrahubGraphQLQueryMutation
23
23
  from infrahub.graphql.mutations.profile import InfrahubProfileMutation
24
+ from infrahub.graphql.types.metadata import OrderInput
24
25
  from infrahub.types import ATTRIBUTE_TYPES, InfrahubDataType, get_attribute_type
25
26
 
27
+ from .constants import NODE_METADATA_TYPE, RELATIONSHIP_METADATA_TYPE
26
28
  from .directives import DIRECTIVES
27
29
  from .enums import generate_graphql_enum, get_enum_attribute_type_name
28
30
  from .metrics import SCHEMA_GENERATE_GRAPHQL_METRICS
@@ -42,6 +44,7 @@ from .mutations.resource_manager import (
42
44
  )
43
45
  from .mutations.webhook import InfrahubWebhookMutation
44
46
  from .registry import registry
47
+ from .resolvers.account_metadata import account_metadata_resolver
45
48
  from .resolvers.ipam import ipam_paginated_list_resolver
46
49
  from .resolvers.resolver import (
47
50
  account_resolver,
@@ -65,8 +68,10 @@ from .types import (
65
68
  )
66
69
  from .types.attribute import BaseAttribute as BaseAttributeType
67
70
  from .types.attribute import TextAttributeType
71
+ from .types.branch import InfrahubBranchEdge
68
72
  from .types.context import ContextInput
69
73
  from .types.event import EVENT_TYPES
74
+ from .types.node import InfrahubObjectWithoutMeta
70
75
 
71
76
  if TYPE_CHECKING:
72
77
  from graphql import GraphQLSchema
@@ -82,10 +87,6 @@ class DeleteInput(graphene.InputObjectType):
82
87
  GraphQLTypes = type[InfrahubMutation] | type[BaseAttributeType] | type[graphene.Interface] | type[graphene.ObjectType]
83
88
 
84
89
 
85
- class OrderInput(graphene.InputObjectType):
86
- disable = graphene.Boolean(required=False)
87
-
88
-
89
90
  @dataclass
90
91
  class GraphqlMutations:
91
92
  create: type[InfrahubMutation]
@@ -243,15 +244,37 @@ class GraphQLSchemaManager:
243
244
  self.set_type(name=event._meta.name, graphql_type=event)
244
245
 
245
246
  def _load_node_interface(self) -> None:
247
+ """Load the base CoreNode interface. Edged/paginated objects are created later in generate_object_types."""
246
248
  node_interface_schema = GenericSchema(
247
249
  name="Node", namespace="Core", description="Interface for all nodes in Infrahub"
248
250
  )
249
- interface = self.generate_interface_object(schema=node_interface_schema, populate_cache=True)
251
+ self.generate_interface_object(schema=node_interface_schema, populate_cache=True)
252
+
253
+ def _complete_node_interface(self, node_metadata: type[InfrahubObject]) -> None:
254
+ """Complete the CoreNode interface by creating its edged and paginated objects."""
255
+ node_interface_schema = GenericSchema(
256
+ name="Node", namespace="Core", description="Interface for all nodes in Infrahub"
257
+ )
258
+ # Re-call generate_interface_object to get the InterfaceReference (will use cached version)
259
+ interface = self.generate_interface_object(schema=node_interface_schema, populate_cache=False)
250
260
  edged_interface = self.generate_graphql_edged_object(
251
- schema=node_interface_schema, node=interface, populate_cache=True
261
+ schema=node_interface_schema, node=interface, node_metadata=node_metadata, populate_cache=True
252
262
  )
253
263
  self.generate_graphql_paginated_object(schema=node_interface_schema, edge=edged_interface, populate_cache=True)
254
264
 
265
+ def _patch_static_types(self, node_metadata: type[InfrahubObject]) -> None:
266
+ """Patch statically defined GraphQL types to use dynamically generated types.
267
+
268
+ Some GraphQL types like InfrahubBranchEdge are defined statically but need to
269
+ reference dynamically generated types (like node_metadata with GenericAccount).
270
+ This method patches those static types after the dynamic types are created.
271
+
272
+ The method checks if the patch has already been applied to avoid redundant updates.
273
+ """
274
+ current_field = InfrahubBranchEdge._meta.fields.get("node_metadata")
275
+ if current_field is None or current_field.type != node_metadata:
276
+ InfrahubBranchEdge._meta.fields["node_metadata"] = graphene.Field(node_metadata, required=True)
277
+
255
278
  def _load_all_enum_types(self, node_schemas: Iterable[MainSchemaTypes]) -> None:
256
279
  for node_schema in node_schemas:
257
280
  self._load_enum_type(node_schema=node_schema)
@@ -311,25 +334,49 @@ class GraphQLSchemaManager:
311
334
 
312
335
  full_schema = self.schema.get_all(duplicate=False)
313
336
 
314
- # Generate all GraphQL Interface Object first and store them in the registry
337
+ # Pass 1: Generate all GraphQL Interface objects first (without edged/paginated)
338
+ # This ensures GENERICACCOUNT exists before we create node_metadata
315
339
  for node_schema in full_schema.values():
316
- if not isinstance(node_schema, GenericSchema):
317
- continue
318
- interface = self.generate_interface_object(schema=node_schema, populate_cache=True)
319
- edged_interface = self.generate_graphql_edged_object(
320
- schema=node_schema, node=interface, populate_cache=True
321
- )
322
- self.generate_graphql_paginated_object(schema=node_schema, edge=edged_interface, populate_cache=True)
340
+ if isinstance(node_schema, GenericSchema):
341
+ self.generate_interface_object(schema=node_schema, populate_cache=True)
323
342
 
324
343
  # Define LineageSource and LineageOwner
325
344
  data_source = self.get_type(name=InfrahubKind.LINEAGESOURCE)
326
345
  data_owner = self.get_type(name=InfrahubKind.LINEAGEOWNER)
327
346
  self.define_relationship_property(data_source=data_source, data_owner=data_owner)
347
+
348
+ # Now that GENERICACCOUNT exists, create node_metadata and relationship_metadata
349
+ account_type = self.get_type(name=InfrahubKind.GENERICACCOUNT)
350
+ self.define_node_metadata(account_type=account_type)
351
+ self.define_relationship_metadata(account_type=account_type)
352
+ node_metadata = self.get_type(name=NODE_METADATA_TYPE)
353
+ relationship_metadata = self.get_type(name=RELATIONSHIP_METADATA_TYPE)
354
+
355
+ # Complete the CoreNode interface (edged/paginated) now that node_metadata exists
356
+ self._complete_node_interface(node_metadata=node_metadata)
357
+
358
+ # Patch statically defined types to use dynamically generated types
359
+ self._patch_static_types(node_metadata=node_metadata)
360
+
328
361
  relationship_property = self.get_type(name="RelationshipProperty")
329
362
  for data_type in ATTRIBUTE_TYPES.values():
330
363
  gql_type = self.get_type(name=data_type.get_graphql_type_name())
331
364
  gql_type._meta.fields["source"] = graphene.Field(data_source)
332
365
  gql_type._meta.fields["owner"] = graphene.Field(data_owner)
366
+ gql_type._meta.fields["updated_by"] = graphene.Field(
367
+ account_type, required=False, resolver=account_metadata_resolver
368
+ )
369
+
370
+ # Pass 2: Generate edged/paginated objects for all GenericSchema interfaces
371
+ for node_schema in full_schema.values():
372
+ if not isinstance(node_schema, GenericSchema):
373
+ continue
374
+ # Re-call generate_interface_object to get the InterfaceReference (will use cached version)
375
+ interface = self.generate_interface_object(schema=node_schema, populate_cache=False)
376
+ edged_interface = self.generate_graphql_edged_object(
377
+ schema=node_schema, node=interface, node_metadata=node_metadata, populate_cache=True
378
+ )
379
+ self.generate_graphql_paginated_object(schema=node_schema, edge=edged_interface, populate_cache=True)
333
380
 
334
381
  # Generate all Nested, Edged and NestedEdged Interfaces and store them in the registry
335
382
  for node_name, node_schema in full_schema.items():
@@ -340,7 +387,9 @@ class GraphQLSchemaManager:
340
387
  nested_edged_interface = self.generate_nested_interface_object(
341
388
  schema=node_schema,
342
389
  base_interface=node_interface,
390
+ node_metadata=node_metadata,
343
391
  relation_property=relationship_property,
392
+ relationship_metadata=relationship_metadata,
344
393
  )
345
394
 
346
395
  nested_interface = self.generate_paginated_interface_object(
@@ -356,11 +405,13 @@ class GraphQLSchemaManager:
356
405
  if isinstance(node_schema, NodeSchema | ProfileSchema | TemplateSchema):
357
406
  node_object_type = self.generate_graphql_object(schema=node_schema, populate_cache=True)
358
407
  node_type_edged = self.generate_graphql_edged_object(
359
- schema=node_schema, node=node_object_type, populate_cache=True
408
+ schema=node_schema, node=node_object_type, node_metadata=node_metadata, populate_cache=True
360
409
  )
361
410
  nested_node_type_edged = self.generate_graphql_edged_object(
362
411
  schema=node_schema,
363
412
  node=node_object_type,
413
+ node_metadata=node_metadata,
414
+ relationship_metadata=relationship_metadata,
364
415
  relation_property=relationship_property,
365
416
  populate_cache=True,
366
417
  )
@@ -547,7 +598,10 @@ class GraphQLSchemaManager:
547
598
  required=False,
548
599
  description="Human friendly identifier",
549
600
  ),
550
- "_updated_at": graphene.DateTime(required=False),
601
+ "_updated_at": graphene.DateTime(
602
+ required=False,
603
+ deprecation_reason="Query the node_metadata field instead. Will be removed in Infrahub 1.9",
604
+ ),
551
605
  "display_label": graphene.String(required=False),
552
606
  "Meta": type("Meta", (object,), meta_attrs),
553
607
  }
@@ -615,7 +669,6 @@ class GraphQLSchemaManager:
615
669
  }
616
670
 
617
671
  main_attrs = {
618
- "is_visible": graphene.Boolean(required=False),
619
672
  "is_protected": graphene.Boolean(required=False),
620
673
  "updated_at": graphene.DateTime(required=False),
621
674
  "source": graphene.Field(data_source),
@@ -627,6 +680,42 @@ class GraphQLSchemaManager:
627
680
 
628
681
  self.set_type(name=type_name, graphql_type=relationship_property)
629
682
 
683
+ def define_node_metadata(self, account_type: type[InfrahubObject]) -> None:
684
+ meta_attrs = {
685
+ "name": NODE_METADATA_TYPE,
686
+ "description": "Defines node metadata information",
687
+ }
688
+
689
+ main_attrs = {
690
+ "created_at": graphene.DateTime(required=False),
691
+ "created_by": graphene.Field(account_type, required=False, resolver=account_metadata_resolver),
692
+ "updated_at": graphene.DateTime(required=False),
693
+ "updated_by": graphene.Field(account_type, required=False, resolver=account_metadata_resolver),
694
+ "Meta": type("Meta", (object,), meta_attrs),
695
+ }
696
+
697
+ node_metadata = type(NODE_METADATA_TYPE, (graphene.ObjectType,), main_attrs)
698
+
699
+ self.set_type(name=NODE_METADATA_TYPE, graphql_type=node_metadata)
700
+
701
+ def define_relationship_metadata(self, account_type: type[InfrahubObject]) -> None:
702
+ meta_attrs = {
703
+ "name": RELATIONSHIP_METADATA_TYPE,
704
+ "description": "Defines relationship metadata information",
705
+ }
706
+
707
+ main_attrs = {
708
+ "created_at": graphene.DateTime(required=False),
709
+ "created_by": graphene.Field(account_type, required=False, resolver=account_metadata_resolver),
710
+ "updated_at": graphene.DateTime(required=False),
711
+ "updated_by": graphene.Field(account_type, required=False, resolver=account_metadata_resolver),
712
+ "Meta": type("Meta", (object,), meta_attrs),
713
+ }
714
+
715
+ relationship_metadata = type(RELATIONSHIP_METADATA_TYPE, (graphene.ObjectType,), main_attrs)
716
+
717
+ self.set_type(name=RELATIONSHIP_METADATA_TYPE, graphql_type=relationship_metadata)
718
+
630
719
  def generate_graphql_mutations(
631
720
  self,
632
721
  schema: NodeSchema | ProfileSchema | TemplateSchema,
@@ -919,8 +1008,16 @@ class GraphQLSchemaManager:
919
1008
  if not top_level:
920
1009
  filters["isnull"] = graphene.Boolean()
921
1010
 
1011
+ if schema.display_label:
1012
+ display_label_schema = schema.get_attribute("display_label")
1013
+ filters.update(
1014
+ get_attribute_type(kind=display_label_schema.kind).get_graphql_filters(
1015
+ name="display_label", include_properties=False, include_isnull=True
1016
+ )
1017
+ )
1018
+
922
1019
  if schema.human_friendly_id and top_level:
923
- # HFID filter limited to top level because we can't filter on HFID for relationships (yet)
1020
+ # NOTE: this can loosen to allow filtering at a non-top level once IFC-2110 is implemented
924
1021
  filters["hfid"] = graphene.List(graphene.String)
925
1022
 
926
1023
  for attr in schema.attributes:
@@ -935,6 +1032,9 @@ class GraphQLSchemaManager:
935
1032
  filters.update(get_attribute_type().get_graphql_filters(name="any"))
936
1033
  filters["partial_match"] = graphene.Boolean()
937
1034
 
1035
+ # Add metadata filters for filtering by created_by, updated_by, created_at, updated_at
1036
+ filters.update(self._generate_metadata_filters())
1037
+
938
1038
  if schema.kind in [InfrahubKind.IPADDRESS, InfrahubKind.IPPREFIX]:
939
1039
  # This is only available for IPAM generics
940
1040
  filters["include_available"] = graphene.Boolean()
@@ -961,10 +1061,53 @@ class GraphQLSchemaManager:
961
1061
 
962
1062
  return filters
963
1063
 
1064
+ def _generate_metadata_filters(self) -> dict[str, Any]:
1065
+ """Generate GraphQL filters for object-level metadata fields.
1066
+
1067
+ These filters allow querying nodes based on their metadata:
1068
+ - created_by: Filter by the account that created the node
1069
+ - updated_by: Filter by the account that last updated the node
1070
+ - created_at: Filter by creation timestamp
1071
+ - updated_at: Filter by last update timestamp
1072
+
1073
+ Returns:
1074
+ dict: Filter definitions with names as keys and graphene types as values
1075
+ """
1076
+ return {
1077
+ # Account-based filters (created_by)
1078
+ "node_metadata__created_by__id": graphene.ID(description="Filter by exact creator account UUID"),
1079
+ "node_metadata__created_by__ids": graphene.List(
1080
+ graphene.ID, description="Filter by list of creator account UUIDs"
1081
+ ),
1082
+ # Account-based filters (updated_by)
1083
+ "node_metadata__updated_by__id": graphene.ID(description="Filter by exact updater account UUID"),
1084
+ "node_metadata__updated_by__ids": graphene.List(
1085
+ graphene.ID, description="Filter by list of updater account UUIDs"
1086
+ ),
1087
+ # DateTime-based filters (created_at)
1088
+ "node_metadata__created_at": graphene.DateTime(description="Filter by exact creation timestamp"),
1089
+ "node_metadata__created_at__before": graphene.DateTime(
1090
+ description="Filter for objects created before this timestamp"
1091
+ ),
1092
+ "node_metadata__created_at__after": graphene.DateTime(
1093
+ description="Filter for objects created after this timestamp"
1094
+ ),
1095
+ # DateTime-based filters (updated_at)
1096
+ "node_metadata__updated_at": graphene.DateTime(description="Filter by exact update timestamp"),
1097
+ "node_metadata__updated_at__before": graphene.DateTime(
1098
+ description="Filter for objects updated before this timestamp"
1099
+ ),
1100
+ "node_metadata__updated_at__after": graphene.DateTime(
1101
+ description="Filter for objects updated after this timestamp"
1102
+ ),
1103
+ }
1104
+
964
1105
  def generate_graphql_edged_object(
965
1106
  self,
966
1107
  schema: MainSchemaTypes,
967
1108
  node: InterfaceReference | InfrahubObjectReference,
1109
+ node_metadata: type[InfrahubObject],
1110
+ relationship_metadata: type[InfrahubObject] | None = None,
968
1111
  relation_property: type[InfrahubObject] | None = None,
969
1112
  populate_cache: bool = False,
970
1113
  ) -> InfrahubEdgedReference:
@@ -987,15 +1130,18 @@ class GraphQLSchemaManager:
987
1130
 
988
1131
  main_attrs: dict[str, Any] = {
989
1132
  "node": graphene.Field(node.reference, required=False),
1133
+ "node_metadata": graphene.Field(node_metadata, required=False),
990
1134
  "Meta": type("Meta", (object,), meta_attrs),
991
1135
  }
992
1136
 
993
1137
  if relation_property:
994
1138
  main_attrs["properties"] = graphene.Field(relation_property, required=False)
1139
+ if relationship_metadata:
1140
+ main_attrs["relationship_metadata"] = graphene.Field(relationship_metadata, required=False)
995
1141
 
996
1142
  graphql_edged_object = registry.get_edge_type(reference_hash=edge_hash, schema_hash=self.schema_hash)
997
1143
  if not graphql_edged_object:
998
- graphql_edged_object = type(object_name, (InfrahubObject,), main_attrs)
1144
+ graphql_edged_object = type(object_name, (InfrahubObjectWithoutMeta,), main_attrs)
999
1145
  registry.set_edge_type(
1000
1146
  reference=graphql_edged_object, reference_hash=edge_hash, schema_hash=self.schema_hash
1001
1147
  )
@@ -1039,7 +1185,7 @@ class GraphQLSchemaManager:
1039
1185
  )
1040
1186
  if not graphql_paginated_object:
1041
1187
  main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
1042
- graphql_paginated_object = type(object_name, (InfrahubObject,), main_attrs)
1188
+ graphql_paginated_object = type(object_name, (InfrahubObjectWithoutMeta,), main_attrs)
1043
1189
  registry.set_paginated_type(
1044
1190
  reference=graphql_paginated_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1045
1191
  )
@@ -1054,6 +1200,8 @@ class GraphQLSchemaManager:
1054
1200
  schema: GenericSchema,
1055
1201
  relation_property: graphene.ObjectType,
1056
1202
  base_interface: graphene.ObjectType,
1203
+ node_metadata: type[InfrahubObject],
1204
+ relationship_metadata: type[InfrahubObject] | None = None,
1057
1205
  populate_cache: bool = False,
1058
1206
  ) -> type[InfrahubObject]:
1059
1207
  meta_attrs: dict[str, Any] = {
@@ -1064,12 +1212,18 @@ class GraphQLSchemaManager:
1064
1212
 
1065
1213
  main_attrs: dict[str, Any] = {
1066
1214
  "node": graphene.Field(base_interface, required=False),
1067
- "_updated_at": graphene.DateTime(required=False),
1215
+ "_updated_at": graphene.DateTime(
1216
+ required=False,
1217
+ deprecation_reason="Query the node_metadata field instead. Will be removed in Infrahub 1.9",
1218
+ ),
1219
+ "node_metadata": graphene.Field(node_metadata, required=True),
1068
1220
  "Meta": type("Meta", (object,), meta_attrs),
1069
1221
  }
1070
1222
 
1071
1223
  if relation_property:
1072
1224
  main_attrs["properties"] = graphene.Field(relation_property, required=False)
1225
+ if relationship_metadata:
1226
+ main_attrs["relationship_metadata"] = graphene.Field(relationship_metadata, required=False)
1073
1227
 
1074
1228
  object_name = f"NestedEdged{schema.kind}"
1075
1229
  md5hash = hashlib.md5(usedforsecurity=False)
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from infrahub.core.constants import MetadataOptions
6
+ from infrahub.core.metadata.model import MetadataQueryOptions
7
+
8
+
9
+ def get_metadata_options_from_fields(fields: dict[str, Any]) -> MetadataOptions:
10
+ """Convert a dict of requested metadata fields to MetadataOptions flags."""
11
+ options = MetadataOptions.NONE
12
+ if "created_at" in fields:
13
+ options |= MetadataOptions.CREATED_AT
14
+ if "created_by" in fields:
15
+ options |= MetadataOptions.CREATED_BY
16
+ if "updated_at" in fields or "_updated_at" in fields:
17
+ options |= MetadataOptions.UPDATED_AT
18
+ if "updated_by" in fields:
19
+ options |= MetadataOptions.UPDATED_BY
20
+ if "source" in fields or "_relation__source" in fields:
21
+ options |= MetadataOptions.SOURCE
22
+ if "owner" in fields or "_relation__owner" in fields:
23
+ options |= MetadataOptions.OWNER
24
+ if "is_protected" in fields:
25
+ options |= MetadataOptions.IS_PROTECTED
26
+ return options
27
+
28
+
29
+ def _extract_attribute_metadata_from_node_fields(node_fields: dict[str, Any]) -> MetadataOptions:
30
+ """Extract attribute-level metadata options from nested node fields.
31
+
32
+ This handles GraphQL query structures like:
33
+ name {
34
+ value
35
+ updated_by { id }
36
+ updated_at
37
+ }
38
+
39
+ Where `updated_by` and `updated_at` are attribute-level metadata.
40
+ """
41
+ attribute_metadata_options = MetadataOptions.NONE
42
+
43
+ for field_properties_dict in node_fields.values():
44
+ if not field_properties_dict or not isinstance(field_properties_dict, dict):
45
+ continue
46
+
47
+ # For attributes, metadata fields are at the same level as 'value'
48
+ # Check if this looks like an attribute (has metadata fields directly)
49
+ metadata_properties_dict = field_properties_dict
50
+
51
+ # Extract metadata from attribute fields
52
+ if "updated_at" in metadata_properties_dict or "_updated_at" in metadata_properties_dict:
53
+ attribute_metadata_options |= MetadataOptions.UPDATED_AT
54
+ if "updated_by" in metadata_properties_dict:
55
+ attribute_metadata_options |= MetadataOptions.UPDATED_BY
56
+ if "created_at" in metadata_properties_dict:
57
+ attribute_metadata_options |= MetadataOptions.CREATED_AT
58
+ if "created_by" in metadata_properties_dict:
59
+ attribute_metadata_options |= MetadataOptions.CREATED_BY
60
+ if "source" in metadata_properties_dict or "_relation__source" in metadata_properties_dict:
61
+ attribute_metadata_options |= MetadataOptions.SOURCE
62
+ if "owner" in metadata_properties_dict or "_relation__owner" in metadata_properties_dict:
63
+ attribute_metadata_options |= MetadataOptions.OWNER
64
+
65
+ return attribute_metadata_options
66
+
67
+
68
+ def build_metadata_query_options(
69
+ node_metadata_fields: dict[str, Any] | None = None,
70
+ relationship_metadata_fields: dict[str, Any] | None = None,
71
+ node_fields: dict[str, Any] | None = None,
72
+ ) -> MetadataQueryOptions:
73
+ """Build MetadataQueryOptions from GraphQL extracted fields.
74
+
75
+ Args:
76
+ node_metadata_fields: Fields from the node_metadata section of a GraphQL query.
77
+ relationship_metadata_fields: Fields from the relationship_metadata section.
78
+ node_fields: The node fields dict to extract attribute-level metadata from.
79
+
80
+ Returns:
81
+ MetadataQueryOptions with appropriate flags set for each level.
82
+ """
83
+ node_level = get_metadata_options_from_fields(node_metadata_fields or {})
84
+ relationship_level = get_metadata_options_from_fields(relationship_metadata_fields or {})
85
+ attribute_level = _extract_attribute_metadata_from_node_fields(node_fields or {})
86
+
87
+ return MetadataQueryOptions(
88
+ node_level=node_level,
89
+ attribute_level=attribute_level,
90
+ relationship_level=relationship_level,
91
+ )
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, Any
2
2
 
3
- from graphene import Boolean, Field, InputField, InputObjectType, Mutation, String
3
+ from graphene import Boolean, Field, InputField, InputObjectType, Mutation, ObjectType, String
4
4
  from graphql import GraphQLResolveInfo
5
5
  from infrahub_sdk.uuidt import UUIDT
6
6
  from typing_extensions import Self
@@ -9,13 +9,13 @@ from infrahub.auth import AuthType
9
9
  from infrahub.core.constants import InfrahubKind
10
10
  from infrahub.core.manager import NodeManager
11
11
  from infrahub.core.node import Node
12
+ from infrahub.core.order import OrderModel
12
13
  from infrahub.core.protocols import CoreAccount, CoreNode, InternalAccountToken
13
14
  from infrahub.core.timestamp import Timestamp
14
15
  from infrahub.database import InfrahubDatabase, retry_db_transaction
15
16
  from infrahub.exceptions import NodeNotFoundError, PermissionDeniedError
16
17
  from infrahub.graphql.field_extractor import extract_graphql_fields
17
18
 
18
- from ..models import OrderModel
19
19
  from ..types import InfrahubObjectType
20
20
 
21
21
  if TYPE_CHECKING:
@@ -36,7 +36,7 @@ class InfrahubAccountUpdateSelfInput(InputObjectType):
36
36
  description = InputField(String(required=False), description="Description to use instead of the current one")
37
37
 
38
38
 
39
- class ValueType(InfrahubObjectType):
39
+ class ValueType(ObjectType):
40
40
  value = String(required=True)
41
41
 
42
42
 
@@ -99,7 +99,7 @@ class AccountMixin:
99
99
  )
100
100
 
101
101
  async with db.start_transaction() as dbt:
102
- await obj.save(db=dbt)
102
+ await obj.save(db=dbt, user_id=account.id)
103
103
 
104
104
  fields = extract_graphql_fields(info=info)
105
105
  return cls(object=await obj.to_graphql(db=db, fields=fields.get("object", {})), ok=True) # type: ignore[call-arg]
@@ -126,7 +126,7 @@ class AccountMixin:
126
126
  raise NodeNotFoundError(node_type="AccountToken", identifier=token_id)
127
127
 
128
128
  async with db.start_transaction() as dbt:
129
- await results[0].delete(db=dbt)
129
+ await results[0].delete(db=dbt, user_id=account.id)
130
130
 
131
131
  return cls(ok=True) # type: ignore[call-arg]
132
132
 
@@ -144,7 +144,7 @@ class AccountMixin:
144
144
  getattr(account, field).value = value
145
145
 
146
146
  async with db.start_transaction() as dbt:
147
- await account.save(db=dbt)
147
+ await account.save(db=dbt, user_id=account.id)
148
148
 
149
149
  return cls(ok=True) # type: ignore[call-arg]
150
150
 
@@ -8,7 +8,6 @@ from infrahub.graphql.types.attribute import GenericPoolInput
8
8
 
9
9
 
10
10
  class BaseAttributeCreate(InputObjectType):
11
- is_visible = Boolean(required=False)
12
11
  is_protected = Boolean(required=False)
13
12
  source = String(required=False)
14
13
  owner = String(required=False)
@@ -21,7 +20,6 @@ class BaseAttributeCreate(InputObjectType):
21
20
 
22
21
  class BaseAttributeUpdate(InputObjectType):
23
22
  is_default = Boolean(required=False)
24
- is_visible = Boolean(required=False)
25
23
  is_protected = Boolean(required=False)
26
24
  source = String(required=False)
27
25
  owner = String(required=False)
@@ -98,7 +98,11 @@ class BranchCreate(Mutation):
98
98
  # Retrieve created branch
99
99
  obj = await Branch.get_by_name(db=graphql_context.db, name=model.name)
100
100
  fields = extract_graphql_fields(info=info)
101
- return cls(object=await obj.to_graphql(fields=fields.get("object", {})), ok=True, task=task)
101
+ return cls(
102
+ object=await obj.to_graphql_flat(fields=fields.get("object", {})),
103
+ ok=True,
104
+ task=task,
105
+ )
102
106
 
103
107
 
104
108
  class BranchNameInput(InputObjectType):
@@ -172,7 +176,7 @@ class BranchUpdate(Mutation):
172
176
  setattr(obj, field_name, data[field_name])
173
177
 
174
178
  async with graphql_context.db.start_transaction() as db:
175
- await obj.save(db=db)
179
+ await obj.save(db=db, user_id=graphql_context.active_account_session.account_id)
176
180
 
177
181
  return cls(ok=True)
178
182
 
@@ -218,7 +222,7 @@ class BranchRebase(Mutation):
218
222
  fields = extract_graphql_fields(info=info)
219
223
  ok = True
220
224
 
221
- return cls(object=await obj.to_graphql(fields=fields.get("object", {})), ok=ok, task=task)
225
+ return cls(object=await obj.to_graphql_flat(fields=fields.get("object", {})), ok=ok, task=task)
222
226
 
223
227
 
224
228
  class BranchValidate(Mutation):
@@ -260,7 +264,7 @@ class BranchValidate(Mutation):
260
264
 
261
265
  fields = extract_graphql_fields(info=info)
262
266
 
263
- return cls(object=await obj.to_graphql(fields=fields.get("object", {})), ok=ok, task=task)
267
+ return cls(object=await obj.to_graphql_flat(fields=fields.get("object", {})), ok=ok, task=task)
264
268
 
265
269
 
266
270
  class BranchMerge(Mutation):
@@ -315,4 +319,4 @@ class BranchMerge(Mutation):
315
319
  fields = extract_graphql_fields(info=info)
316
320
  ok = True
317
321
 
318
- return cls(object=await obj.to_graphql(fields=fields.get("object", {})), ok=ok, task=task)
322
+ return cls(object=await obj.to_graphql_flat(fields=fields.get("object", {})), ok=ok, task=task)
@@ -102,7 +102,7 @@ class UpdateComputedAttribute(Mutation):
102
102
  if attribute_field.value != str(data.value):
103
103
  attribute_field.value = str(data.value)
104
104
  async with graphql_context.db.start_transaction() as dbt:
105
- await target_node.save(db=dbt, fields=[str(data.attribute)])
105
+ await target_node.save(db=dbt, fields=[str(data.attribute)], user_id=graphql_context.assigned_user_id)
106
106
 
107
107
  log_data = get_log_data()
108
108
  request_id = log_data.get("request_id", "")
@@ -93,7 +93,7 @@ class UpdateDisplayLabel(Mutation):
93
93
  await target_node.set_display_label(value=str(data.value))
94
94
 
95
95
  async with graphql_context.db.start_transaction() as dbt:
96
- await target_node.save(db=dbt, fields=["display_label"])
96
+ await target_node.save(db=dbt, fields=["display_label"], user_id=graphql_context.assigned_user_id)
97
97
 
98
98
  log_data = get_log_data()
99
99
  request_id = log_data.get("request_id", "")
@@ -100,7 +100,7 @@ class UpdateHFID(Mutation):
100
100
  await target_node.set_human_friendly_id(value=updated_hfid)
101
101
 
102
102
  async with graphql_context.db.start_transaction() as dbt:
103
- await target_node.save(db=dbt, fields=["human_friendly_id"])
103
+ await target_node.save(db=dbt, fields=["human_friendly_id"], user_id=graphql_context.assigned_user_id)
104
104
 
105
105
  log_data = get_log_data()
106
106
  request_id = log_data.get("request_id", "")
@@ -9,7 +9,7 @@ from typing_extensions import Self
9
9
  from infrahub import lock
10
10
  from infrahub.core import registry
11
11
  from infrahub.core.branch import Branch
12
- from infrahub.core.constants import InfrahubKind
12
+ from infrahub.core.constants import InfrahubKind, MetadataOptions
13
13
  from infrahub.core.ipam.reconciler import IpamReconciler
14
14
  from infrahub.core.manager import NodeManager
15
15
  from infrahub.core.node import Node
@@ -196,8 +196,7 @@ class InfrahubIPAddressMutation(InfrahubMutationMixin, Mutation):
196
196
  kind=cls._meta.schema.kind,
197
197
  id=data.get("id"),
198
198
  branch=branch,
199
- include_owner=True,
200
- include_source=True,
199
+ include_metadata=MetadataOptions.LINKED_NODES,
201
200
  )
202
201
  namespace = await address.ip_namespace.get_peer(db)
203
202
  namespace_id = await validate_namespace(db=db, branch=branch, data=data, existing_namespace_id=namespace.id)
@@ -356,8 +355,7 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
356
355
  kind=cls._meta.schema.kind,
357
356
  id=data.get("id"),
358
357
  branch=branch,
359
- include_owner=True,
360
- include_source=True,
358
+ include_metadata=MetadataOptions.LINKED_NODES,
361
359
  )
362
360
  namespace = await prefix.ip_namespace.get_peer(db)
363
361
  namespace_id = await validate_namespace(db=db, branch=branch, data=data, existing_namespace_id=namespace.id)
@@ -435,7 +433,7 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
435
433
  data.get("id"), graphql_context.db, branch=branch, prefetch_relationships=True
436
434
  )
437
435
  if not prefix:
438
- raise NodeNotFoundError(branch, cls._meta.schema.kind, data.get("id"))
436
+ raise NodeNotFoundError(node_type=cls._meta.schema.kind, identifier=data.get("id"), branch_name=branch.name)
439
437
 
440
438
  namespace_rels = await prefix.ip_namespace.get_relationships(db=db)
441
439
  namespace_id = namespace_rels[0].peer_id