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
@@ -150,7 +150,8 @@ class InfrahubMutationMixin:
150
150
  database: InfrahubDatabase | None = None,
151
151
  override_data: dict[str, Any] | None = None,
152
152
  ) -> tuple[Node, Self]:
153
- db = database or info.context.db
153
+ graphql_context: GraphqlContext = info.context
154
+ db = database or graphql_context.db
154
155
  schema = cls._meta.active_schema
155
156
 
156
157
  create_data = dict(data)
@@ -161,6 +162,7 @@ class InfrahubMutationMixin:
161
162
  db=db,
162
163
  branch=branch,
163
164
  schema=schema,
165
+ user_id=graphql_context.assigned_user_id,
164
166
  )
165
167
 
166
168
  graphql_response = await build_graphql_response(info=info, db=db, obj=obj)
@@ -239,12 +241,13 @@ class InfrahubMutationMixin:
239
241
  async def mutate_update_object(
240
242
  cls,
241
243
  db: InfrahubDatabase,
242
- info: GraphQLResolveInfo, # noqa: ARG003
244
+ info: GraphQLResolveInfo,
243
245
  data: InputObjectType,
244
246
  branch: Branch,
245
247
  obj: Node,
246
248
  skip_uniqueness_check: bool = False,
247
249
  ) -> Node:
250
+ graphql_context: GraphqlContext = info.context
248
251
  component_registry = get_component_registry()
249
252
  node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
250
253
 
@@ -264,7 +267,7 @@ class InfrahubMutationMixin:
264
267
  node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
265
268
  updated_field_names = await node_profiles_applier.apply_profiles(node=obj)
266
269
  fields += updated_field_names
267
- await obj.save(db=db, fields=fields)
270
+ await obj.save(db=db, fields=fields, user_id=graphql_context.assigned_user_id)
268
271
 
269
272
  return obj
270
273
 
@@ -381,7 +384,9 @@ class InfrahubMutationMixin:
381
384
  async def _delete_obj(cls, graphql_context: GraphqlContext, branch: Branch, obj: Node) -> list[Node]:
382
385
  db = graphql_context.db
383
386
  async with db.start_transaction() as dbt:
384
- deleted = await NodeManager.delete(db=dbt, branch=branch, nodes=[obj])
387
+ deleted = await NodeManager.delete(
388
+ db=dbt, branch=branch, nodes=[obj], user_id=graphql_context.assigned_user_id
389
+ )
385
390
  deleted_str = ", ".join([f"{d.get_kind()}({d.get_id()})" for d in deleted])
386
391
  log.info(f"nodes deleted: {deleted_str}")
387
392
  return deleted
@@ -7,8 +7,11 @@ from graphql import GraphQLResolveInfo
7
7
  from opentelemetry import trace
8
8
  from typing_extensions import Self
9
9
 
10
+ from infrahub.core.constants import MetadataOptions
10
11
  from infrahub.core.manager import NodeManager
12
+ from infrahub.core.relationship.constraints.profiles_removal import RelationshipProfileRemovalConstraint
11
13
  from infrahub.core.schema import ProfileSchema
14
+ from infrahub.dependencies.registry import get_component_registry
12
15
  from infrahub.graphql.types.context import ContextInput
13
16
  from infrahub.log import get_logger
14
17
  from infrahub.profiles.node_applier import NodeProfilesApplier
@@ -69,16 +72,6 @@ class InfrahubProfileMutation(InfrahubMutationMixin, Mutation):
69
72
  },
70
73
  )
71
74
 
72
- @classmethod
73
- def _get_profile_attr_values_map(cls, obj: Node) -> dict[str, Any]:
74
- attr_values_map = {}
75
- for attr_schema in obj.get_schema().attributes:
76
- # profile name update can be ignored
77
- if attr_schema.name == "profile_name":
78
- continue
79
- attr_values_map[attr_schema.name] = getattr(obj, attr_schema.name).value
80
- return attr_values_map
81
-
82
75
  @classmethod
83
76
  async def _get_profile_related_node_ids(cls, db: InfrahubDatabase, obj: Node) -> set[str]:
84
77
  related_nodes = []
@@ -126,29 +119,24 @@ class InfrahubProfileMutation(InfrahubMutationMixin, Mutation):
126
119
  skip_uniqueness_check: bool = False,
127
120
  ) -> tuple[Node, Self]:
128
121
  workflow_service = info.context.active_service.workflow
129
- original_attr_values = cls._get_profile_attr_values_map(obj=obj)
130
122
  original_related_node_ids = await cls._get_profile_related_node_ids(db=db, obj=obj)
131
123
 
132
124
  obj, mutation = await super()._call_mutate_update(
133
125
  info=info, data=data, branch=branch, db=db, obj=obj, skip_uniqueness_check=skip_uniqueness_check
134
126
  )
135
127
 
136
- updated_attr_values = cls._get_profile_attr_values_map(obj=obj)
137
128
  updated_related_node_ids = await cls._get_profile_related_node_ids(db=db, obj=obj)
138
129
 
139
- if original_attr_values != updated_attr_values:
140
- await cls._send_profile_refresh_workflows(
141
- db=db, workflow_service=workflow_service, branch_name=branch.name, obj=obj
142
- )
143
- elif updated_related_node_ids != original_related_node_ids:
144
- removed_node_ids = original_related_node_ids - updated_related_node_ids
145
- added_node_ids = updated_related_node_ids - original_related_node_ids
130
+ # Handle nodes removed from related_nodes - these need explicit refresh
131
+ # since the async automation won't find them in the profile's related_nodes after the change.
132
+ # Attribute changes and added nodes are handled by the Prefect automation
133
+ if removed_node_ids := original_related_node_ids - updated_related_node_ids:
146
134
  await cls._send_profile_refresh_workflows(
147
135
  db=db,
148
136
  workflow_service=workflow_service,
149
137
  branch_name=branch.name,
150
138
  obj=obj,
151
- node_ids=list(removed_node_ids) + list(added_node_ids),
139
+ node_ids=list(removed_node_ids),
152
140
  )
153
141
 
154
142
  return obj, mutation
@@ -158,6 +146,12 @@ class InfrahubProfileMutation(InfrahubMutationMixin, Mutation):
158
146
  db = graphql_context.db
159
147
  workflow_service = graphql_context.active_service.workflow
160
148
  related_node_ids = await cls._get_profile_related_node_ids(db=db, obj=obj)
149
+
150
+ profile_schema: ProfileSchema = obj.get_schema() # type: ignore[assignment]
151
+ component_registry = get_component_registry()
152
+ constraint = await component_registry.get_component(RelationshipProfileRemovalConstraint, db=db, branch=branch)
153
+ await constraint.validate_profile_deletion(profile=obj, profile_schema=profile_schema)
154
+
161
155
  deleted = await super()._delete_obj(graphql_context=graphql_context, branch=branch, obj=obj)
162
156
  await cls._send_profile_refresh_workflows(
163
157
  db=db, workflow_service=workflow_service, branch_name=branch.name, obj=obj, node_ids=list(related_node_ids)
@@ -192,12 +186,12 @@ class InfrahubProfilesRefresh(Mutation):
192
186
  db=db,
193
187
  branch=branch,
194
188
  id=str(data.id),
195
- include_source=True,
189
+ include_metadata=MetadataOptions.SOURCE,
196
190
  raise_on_error=True,
197
191
  )
198
192
  node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
199
193
  updated_fields = await node_profiles_applier.apply_profiles(node=obj)
200
194
  if updated_fields:
201
- await obj.save(db=db, fields=updated_fields)
195
+ await obj.save(db=db, fields=updated_fields, user_id=graphql_context.assigned_user_id)
202
196
 
203
197
  return cls(ok=True)
@@ -13,6 +13,7 @@ from infrahub.core.constants import (
13
13
  CheckType,
14
14
  GlobalPermissions,
15
15
  InfrahubKind,
16
+ MetadataOptions,
16
17
  PermissionDecision,
17
18
  )
18
19
  from infrahub.core.manager import NodeManager
@@ -128,8 +129,7 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
128
129
  kind=cls._meta.schema.kind,
129
130
  id=data.get("id"),
130
131
  branch=branch,
131
- include_owner=True,
132
- include_source=True,
132
+ include_metadata=MetadataOptions.LINKED_NODES,
133
133
  )
134
134
  state = ProposedChangeState(obj.state.value.value)
135
135
  state.validate_updatable()
@@ -292,7 +292,7 @@ class ProposedChangeReview(Mutation):
292
292
  current_user=current_user,
293
293
  context=graphql_context,
294
294
  )
295
- await proposed_change.save(db=db)
295
+ await proposed_change.save(db=db, user_id=graphql_context.active_account_session.account_id)
296
296
 
297
297
  if event:
298
298
  event_service = await get_event_service()
@@ -426,7 +426,7 @@ class ProposedChangeMerge(Mutation):
426
426
 
427
427
  async with graphql_context.db.start_session() as db:
428
428
  proposed_change.state.value = ProposedChangeState.MERGING.value
429
- await proposed_change.save(db=db)
429
+ await proposed_change.save(db=db, user_id=graphql_context.assigned_user_id)
430
430
 
431
431
  if wait_until_completion:
432
432
  await graphql_context.service.workflow.execute_workflow(
@@ -11,6 +11,7 @@ from infrahub.core.account import GlobalPermission, ObjectPermission
11
11
  from infrahub.core.changelog.models import NodeChangelog, RelationshipChangelogGetter
12
12
  from infrahub.core.constants import (
13
13
  InfrahubKind,
14
+ MetadataOptions,
14
15
  PermissionAction,
15
16
  PermissionDecision,
16
17
  RelationshipCardinality,
@@ -22,7 +23,7 @@ from infrahub.core.query.relationship import (
22
23
  RelationshipPeerData,
23
24
  )
24
25
  from infrahub.core.relationship import Relationship
25
- from infrahub.database import retry_db_transaction
26
+ from infrahub.database import InfrahubDatabase, retry_db_transaction
26
27
  from infrahub.events import EventMeta
27
28
  from infrahub.events.group_action import GroupMemberAddedEvent, GroupMemberRemovedEvent
28
29
  from infrahub.events.models import EventNode
@@ -32,12 +33,14 @@ from infrahub.graphql.context import apply_external_context
32
33
  from infrahub.graphql.types.context import ContextInput
33
34
  from infrahub.groups.ancestors import collect_ancestors
34
35
  from infrahub.permissions import get_global_permission_for_kind
36
+ from infrahub.profiles.node_applier import NodeProfilesApplier
35
37
 
36
38
  from ..types import RelatedNodeInput
37
39
 
38
40
  if TYPE_CHECKING:
39
41
  from graphql import GraphQLResolveInfo
40
42
 
43
+ from infrahub.core.branch import Branch
41
44
  from infrahub.core.node import Node
42
45
  from infrahub.core.relationship import RelationshipManager
43
46
  from infrahub.core.schema.relationship_schema import RelationshipSchema
@@ -91,7 +94,7 @@ class RelationshipAdd(Mutation):
91
94
  await apply_external_context(graphql_context=graphql_context, context_input=context)
92
95
 
93
96
  rel_schema = source.get_schema().get_relationship(name=relationship_name)
94
- display_label: str = await source.get_display_label(db=graphql_context.db) or ""
97
+ display_label = await source.get_display_label(db=graphql_context.db)
95
98
  node_changelog = NodeChangelog(
96
99
  node_id=source.get_id(), node_kind=source.get_kind(), display_label=display_label
97
100
  )
@@ -107,7 +110,9 @@ class RelationshipAdd(Mutation):
107
110
  for node_data in data.get("nodes"):
108
111
  # Instantiate and resolve a relationship
109
112
  # This will take care of allocating a node from a pool if needed
110
- rel = Relationship(schema=rel_schema, branch=graphql_context.branch, node=source)
113
+ rel = Relationship(
114
+ schema=rel_schema, branch=graphql_context.branch, source_kind=source.get_kind(), node=source
115
+ )
111
116
  await rel.new(db=db, data=node_data)
112
117
  await rel.resolve(db=db)
113
118
  # Save it only if it does not exist
@@ -115,7 +120,14 @@ class RelationshipAdd(Mutation):
115
120
  if group_event_type != GroupUpdateType.NONE:
116
121
  peers.append(EventNode(id=rel.get_peer_id(), kind=nodes[rel.get_peer_id()].get_kind()))
117
122
  node_changelog.create_relationship(relationship=rel)
118
- await rel.save(db=db)
123
+ await rel.save(db=db, user_id=graphql_context.assigned_user_id)
124
+
125
+ if relationship_name == "profiles":
126
+ await _apply_profiles(node=source, db=db, branch=graphql_context.branch)
127
+
128
+ if source.get_schema().is_profile_schema and relationship_name == "related_nodes":
129
+ for node in nodes.values():
130
+ await _apply_profiles(node=node, db=db, branch=graphql_context.branch)
119
131
 
120
132
  if config.SETTINGS.broker.enable and graphql_context.background and node_changelog.has_changes:
121
133
  if group_event_type == GroupUpdateType.MEMBERS:
@@ -230,14 +242,23 @@ class RelationshipRemove(Mutation):
230
242
  for node_data in data.get("nodes"):
231
243
  if node_data.get("id") in existing_peers.keys():
232
244
  # TODO once https://github.com/opsmill/infrahub/issues/792 has been fixed
233
- # we should use RelationshipDataDeleteQuery to delete the relationship
245
+ # we should use RelationshipDeleteQuery to delete the relationship
234
246
  # it would be more query efficient
235
- rel = Relationship(schema=rel_schema, branch=graphql_context.branch, node=source)
247
+ rel = Relationship(
248
+ schema=rel_schema, branch=graphql_context.branch, source_kind=source.get_kind(), node=source
249
+ )
236
250
  rel.load(db=db, data=existing_peers[node_data.get("id")])
237
251
  if group_event_type != GroupUpdateType.NONE:
238
252
  peers.append(EventNode(id=rel.get_peer_id(), kind=nodes[rel.get_peer_id()].get_kind()))
239
253
  node_changelog.delete_relationship(relationship=rel)
240
- await rel.delete(db=db)
254
+ await rel.delete(db=db, user_id=graphql_context.assigned_user_id)
255
+
256
+ if relationship_name == "profiles":
257
+ await _apply_profiles(node=source, db=db, branch=graphql_context.branch)
258
+
259
+ if source.get_schema().is_profile_schema and relationship_name == "related_nodes":
260
+ for node in nodes.values():
261
+ await _apply_profiles(node=node, db=db, branch=graphql_context.branch)
241
262
 
242
263
  if config.SETTINGS.broker.enable and graphql_context.background and node_changelog.has_changes:
243
264
  if group_event_type == GroupUpdateType.MEMBERS:
@@ -318,8 +339,7 @@ async def _validate_node(info: GraphQLResolveInfo, data: RelationshipNodesInput)
318
339
  db=graphql_context.db,
319
340
  id=input_id,
320
341
  branch=graphql_context.branch,
321
- include_owner=False,
322
- include_source=False,
342
+ include_metadata=MetadataOptions.NONE,
323
343
  )
324
344
  ):
325
345
  raise NodeNotFoundError(node_type="node", identifier=input_id, branch_name=graphql_context.branch.name)
@@ -455,7 +475,9 @@ async def _collect_current_peers(
455
475
  query = await RelationshipGetPeerQuery.init(
456
476
  db=graphql_context.db,
457
477
  source=source_node,
458
- rel=Relationship(schema=rel_schema, branch=graphql_context.branch, node=source_node),
478
+ rel=Relationship(
479
+ schema=rel_schema, branch=graphql_context.branch, source_kind=source_node.get_kind(), node=source_node
480
+ ),
459
481
  )
460
482
  await query.execute(db=graphql_context.db)
461
483
  return {str(peer.peer_id): peer for peer in query.get_peers()}
@@ -474,3 +496,11 @@ def _get_group_event_type(
474
496
  # Modifying the membership of the current node
475
497
  group_event_type = GroupUpdateType.MEMBER_OF_GROUPS
476
498
  return group_event_type
499
+
500
+
501
+ async def _apply_profiles(node: Node, db: InfrahubDatabase, branch: Branch) -> None:
502
+ refreshed_node = await NodeManager.get_one(db=db, id=node.get_id(), branch=branch)
503
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
504
+ updated_fields = await node_profiles_applier.apply_profiles(node=refreshed_node)
505
+ if updated_fields:
506
+ await refreshed_node.save(db=db, fields=updated_fields)
@@ -7,7 +7,7 @@ import httpx
7
7
  from graphene import Boolean, Field, InputObjectType, Mutation, String
8
8
 
9
9
  from infrahub import config
10
- from infrahub.core.constants import InfrahubKind
10
+ from infrahub.core.constants import InfrahubKind, MetadataOptions
11
11
  from infrahub.core.manager import NodeManager
12
12
  from infrahub.core.schema import NodeSchema
13
13
  from infrahub.git.models import (
@@ -42,7 +42,9 @@ log = get_logger()
42
42
 
43
43
  class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
44
44
  @classmethod
45
- def __init_subclass_with_meta__(cls, schema: NodeSchema | None = None, _meta=None, **options) -> None:
45
+ def __init_subclass_with_meta__(
46
+ cls, schema: NodeSchema | None = None, _meta: InfrahubMutationOptions | None = None, **options: Any
47
+ ) -> None:
46
48
  # Make sure schema is a valid NodeSchema Node Class
47
49
  if not isinstance(schema, NodeSchema):
48
50
  raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
@@ -95,21 +97,21 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
95
97
  graphql_context: GraphqlContext = info.context
96
98
 
97
99
  cleanup_payload(data)
98
- if not node:
99
- node: CoreReadOnlyRepository | CoreRepository = await NodeManager.get_one_by_id_or_default_filter(
100
+ repo_node: CoreReadOnlyRepository | CoreRepository | Node | None = node
101
+ if not repo_node:
102
+ repo_node = await NodeManager.get_one_by_id_or_default_filter(
100
103
  db=graphql_context.db,
101
104
  kind=cls._meta.schema.kind,
102
105
  id=data.get("id"),
103
106
  branch=branch,
104
- include_owner=True,
105
- include_source=True,
107
+ include_metadata=MetadataOptions.LINKED_NODES,
106
108
  )
107
- if node.get_kind() != InfrahubKind.READONLYREPOSITORY:
108
- return await super().mutate_update(info, data, branch, database=graphql_context.db, node=node)
109
+ if repo_node.get_kind() != InfrahubKind.READONLYREPOSITORY:
110
+ return await super().mutate_update(info, data, branch, database=graphql_context.db, node=repo_node)
109
111
 
110
- node = cast("CoreReadOnlyRepository", node)
111
- current_commit = node.commit.value
112
- current_ref = node.ref.value
112
+ repo_node = cast("CoreReadOnlyRepository", repo_node)
113
+ current_commit = repo_node.commit.value
114
+ current_ref = repo_node.ref.value
113
115
  new_commit = None
114
116
  if data.commit and data.commit.value:
115
117
  new_commit = data.commit.value
@@ -117,7 +119,7 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
117
119
  if data.ref and data.ref.value:
118
120
  new_ref = data.ref.value
119
121
 
120
- obj, result = await super().mutate_update(info, data, branch, database=graphql_context.db, node=node)
122
+ obj, result = await super().mutate_update(info, data, branch, database=graphql_context.db, node=repo_node)
121
123
  obj = cast("CoreReadOnlyRepository", obj)
122
124
 
123
125
  send_update_message = (new_commit and new_commit != current_commit) or (new_ref and new_ref != current_ref)
@@ -329,11 +329,11 @@ async def update_registry(
329
329
  log.info(f"Schema has diff, will need to be updated {diff.all}", branch=branch.name)
330
330
  async with db.start_transaction() as dbt:
331
331
  await registry.schema.update_schema_branch(
332
- schema=tmp_schema, db=dbt, branch=branch.name, limit=diff.all, update_db=True
332
+ schema=tmp_schema, db=dbt, branch=branch.name, limit=diff.all, update_db=True, user_id=account_id
333
333
  )
334
334
  branch.update_schema_hash()
335
335
  log.info("Schema has been updated", branch=branch.name, hash=branch.active_schema_hash.main)
336
- await branch.save(db=dbt)
336
+ await branch.save(db=dbt, user_id=account_id)
337
337
 
338
338
  await service.component.refresh_schema_hash(branches=[branch.name])
339
339
 
@@ -2,24 +2,59 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from graphene import ID, Field, Int, List, NonNull, String
5
+ from graphene import ID, Argument, Boolean, Field, Int, List, NonNull, String
6
6
 
7
+ from infrahub.constants.enums import OrderByField, OrderDirection
8
+ from infrahub.core.node.standard import StandardNodeOrdering, StandardNodeQueryFields
7
9
  from infrahub.core.registry import registry
8
10
  from infrahub.exceptions import ValidationError
9
11
  from infrahub.graphql.field_extractor import extract_graphql_fields
10
12
  from infrahub.graphql.types import BranchType, InfrahubBranch, InfrahubBranchType
13
+ from infrahub.graphql.types.metadata import OrderInput
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  from graphql import GraphQLResolveInfo
14
17
 
15
18
 
19
+ def standard_node_ordering_from_order_input(order: OrderInput | None = None) -> StandardNodeOrdering:
20
+ """Create a StandardNodeOrdering from an OrderInput.
21
+
22
+ Args:
23
+ order: Optional ordering specification from GraphQL input.
24
+
25
+ Returns:
26
+ StandardNodeOrdering with the specified field and direction, or defaults to ID with no direction.
27
+
28
+ Raises:
29
+ ValidationError: If both created_at and updated_at are specified.
30
+ """
31
+ if order is None or not order.node_metadata:
32
+ return StandardNodeOrdering()
33
+
34
+ created_at = getattr(order.node_metadata, "created_at", None)
35
+ updated_at = getattr(order.node_metadata, "updated_at", None)
36
+
37
+ if created_at and updated_at:
38
+ raise ValidationError("Only one of 'created_at' or 'updated_at' can be specified for ordering.")
39
+
40
+ if created_at:
41
+ return StandardNodeOrdering(order_by=OrderByField.CREATED_AT, direction=OrderDirection(created_at.value))
42
+
43
+ if updated_at:
44
+ return StandardNodeOrdering(order_by=OrderByField.UPDATED_AT, direction=OrderDirection(updated_at.value))
45
+
46
+ return StandardNodeOrdering()
47
+
48
+
16
49
  async def branch_resolver(
17
50
  root: dict, # noqa: ARG001
18
51
  info: GraphQLResolveInfo,
19
52
  **kwargs: Any,
20
53
  ) -> list[dict[str, Any]]:
21
54
  fields = extract_graphql_fields(info)
22
- return await BranchType.get_list(graphql_context=info.context, fields=fields, exclude_global=True, **kwargs)
55
+ return await BranchType.get_list(
56
+ graphql_context=info.context, fields=StandardNodeQueryFields(node=fields), exclude_global=True, **kwargs
57
+ )
23
58
 
24
59
 
25
60
  BranchQueryList = Field(
@@ -39,36 +74,51 @@ async def infrahub_branch_resolver(
39
74
  offset: int | None = None,
40
75
  name__value: str | None = None,
41
76
  ids: list[str] | None = None,
77
+ partial_match: bool = False,
78
+ order: OrderInput | None = None,
42
79
  ) -> dict[str, Any]:
43
80
  if isinstance(limit, int) and limit < 1:
44
81
  raise ValidationError("limit must be >= 1")
45
82
  if isinstance(offset, int) and offset < 0:
46
83
  raise ValidationError("offset must be >= 0")
47
84
 
85
+ node_ordering = standard_node_ordering_from_order_input(order)
86
+
48
87
  fields = extract_graphql_fields(info)
49
88
  result: dict[str, Any] = {}
50
89
  if "edges" in fields:
90
+ query_fields = StandardNodeQueryFields(
91
+ node=fields.get("edges", {}).get("node", {}),
92
+ node_metadata=fields.get("edges", {}).get("node_metadata", {}),
93
+ )
51
94
  branches = await InfrahubBranch.get_list(
52
95
  graphql_context=info.context,
53
- fields=fields.get("edges", {}).get("node", {}),
96
+ fields=query_fields,
54
97
  limit=limit,
55
98
  offset=offset,
56
99
  name=name__value,
57
100
  ids=ids,
58
101
  exclude_global=True,
102
+ partial_match=partial_match,
103
+ node_ordering=node_ordering,
59
104
  )
60
- result["edges"] = [{"node": branch} for branch in branches]
105
+ result["edges"] = branches
61
106
  if "count" in fields:
62
107
  result["count"] = await InfrahubBranchType.get_list_count(
63
- graphql_context=info.context, name=name__value, ids=ids
108
+ graphql_context=info.context,
109
+ name=name__value,
110
+ ids=ids,
111
+ partial_match=partial_match,
112
+ node_ordering=node_ordering,
64
113
  )
65
114
 
66
115
  if "default_branch" in fields:
67
- result["default_branch"] = await InfrahubBranch.get_by_name(
116
+ default_branch = await InfrahubBranch.get_by_name(
68
117
  graphql_context=info.context,
69
118
  fields=fields["default_branch"],
70
119
  name=registry.default_branch,
71
120
  )
121
+ result["default_branch"] = default_branch["node"]
72
122
 
73
123
  return result
74
124
 
@@ -79,6 +129,12 @@ InfrahubBranchQueryList = Field(
79
129
  limit=Int(),
80
130
  name__value=String(),
81
131
  ids=List(ID),
132
+ partial_match=Boolean(default_value=False),
133
+ order=Argument(
134
+ OrderInput,
135
+ required=False,
136
+ description="Define ordering of results for branch queries.",
137
+ ),
82
138
  description="Retrieve paginated information about active branches.",
83
139
  resolver=infrahub_branch_resolver,
84
140
  required=True,
@@ -60,10 +60,10 @@ class ConflictDetails(ObjectType):
60
60
 
61
61
 
62
62
  class DiffSummaryCounts(ObjectType):
63
- num_added = Int(required=True)
64
- num_updated = Int(required=True)
65
- num_removed = Int(required=True)
66
- num_conflicts = Int(required=True)
63
+ num_added = Int(required=False)
64
+ num_updated = Int(required=False)
65
+ num_removed = Int(required=False)
66
+ num_conflicts = Int(required=False)
67
67
 
68
68
 
69
69
  class DiffProperty(ObjectType):
@@ -146,7 +146,7 @@ class DiffTreeSummary(DiffSummaryCounts):
146
146
  diff_branch = String(required=True)
147
147
  from_time = DateTime(required=True)
148
148
  to_time = DateTime(required=True)
149
- num_unchanged = Int(required=True)
149
+ num_unchanged = Int(required=False)
150
150
  num_untracked_base_changes = Int(required=False)
151
151
  num_untracked_diff_changes = Int(required=False)
152
152
 
@@ -0,0 +1,84 @@
1
+ """Resolvers for account metadata fields (created_by, updated_by)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from infrahub.graphql.field_extractor import extract_graphql_fields
8
+ from infrahub.graphql.loaders.account import AccountDataLoader, AccountLoaderParams
9
+
10
+ if TYPE_CHECKING:
11
+ from graphql import GraphQLResolveInfo
12
+
13
+ from infrahub.core.branch.models import Branch
14
+ from infrahub.core.timestamp import Timestamp
15
+ from infrahub.database import InfrahubDatabase
16
+ from infrahub.graphql.initialization import GraphqlContext
17
+
18
+
19
+ class AccountMetadataResolver:
20
+ """Resolver class for account metadata fields (created_by, updated_by).
21
+
22
+ This class maintains DataLoader instances to enable batching and caching
23
+ of account lookups across multiple fields within the same request.
24
+ """
25
+
26
+ def __init__(self) -> None:
27
+ self._data_loader_instances: dict[AccountLoaderParams, AccountDataLoader] = {}
28
+
29
+ def _get_or_create_loader(
30
+ self,
31
+ db: InfrahubDatabase,
32
+ branch: Branch,
33
+ at: Timestamp | None,
34
+ fields: dict[str, Any],
35
+ ) -> AccountDataLoader:
36
+ """Get an existing loader or create a new one for the given parameters."""
37
+ params = AccountLoaderParams(branch=branch, at=at, fields=fields)
38
+
39
+ if params not in self._data_loader_instances:
40
+ self._data_loader_instances[params] = AccountDataLoader(db=db, params=params)
41
+
42
+ return self._data_loader_instances[params]
43
+
44
+ async def resolve(
45
+ self,
46
+ parent: dict[str, Any],
47
+ info: GraphQLResolveInfo,
48
+ ) -> dict[str, Any] | None:
49
+ """Resolve created_by/updated_by fields in metadata objects.
50
+
51
+ The parent dict should contain {"id": "account-uuid", "__kind__": "CoreAccount"}
52
+ for the field being resolved, or the field value may be None.
53
+ """
54
+ field_name = info.field_name # "created_by" or "updated_by"
55
+ account_data = parent.get(field_name)
56
+
57
+ if not account_data or not account_data.get("id"):
58
+ return None
59
+
60
+ account_id = account_data["id"]
61
+ graphql_context: GraphqlContext = info.context
62
+
63
+ # Extract the fields requested for this account
64
+ fields = extract_graphql_fields(info=info)
65
+
66
+ # Get or create a loader for these parameters
67
+ loader = self._get_or_create_loader(
68
+ db=graphql_context.db,
69
+ branch=graphql_context.branch,
70
+ at=graphql_context.at,
71
+ fields=fields,
72
+ )
73
+
74
+ return await loader.load(account_id)
75
+
76
+
77
+ async def account_metadata_resolver(
78
+ parent: dict[str, Any],
79
+ info: GraphQLResolveInfo,
80
+ ) -> dict[str, Any] | None:
81
+ """Function resolver that delegates to the AccountMetadataResolver on the context."""
82
+ graphql_context: GraphqlContext = info.context
83
+ resolver = graphql_context.account_metadata_resolver
84
+ return await resolver.resolve(parent=parent, info=info)