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
@@ -22,7 +22,9 @@ from pydantic import BaseModel, Field
22
22
 
23
23
  from infrahub.core import registry
24
24
  from infrahub.core.changelog.models import ChangelogRelationshipMapper
25
- from infrahub.core.constants import BranchSupportType, InfrahubKind, RelationshipKind
25
+ from infrahub.core.constants import SYSTEM_USER_ID, BranchSupportType, InfrahubKind, MetadataOptions, RelationshipKind
26
+ from infrahub.core.metadata.interface import MetadataInterface
27
+ from infrahub.core.metadata.model import MetadataInfo, MetadataQueryOptions
26
28
  from infrahub.core.property import (
27
29
  FlagPropertyMixin,
28
30
  NodePropertyData,
@@ -30,16 +32,13 @@ from infrahub.core.property import (
30
32
  )
31
33
  from infrahub.core.query.relationship import (
32
34
  RelationshipCreateQuery,
33
- RelationshipDataDeleteQuery,
34
35
  RelationshipDeleteQuery,
35
36
  RelationshipGetPeerQuery,
36
- RelationshipGetQuery,
37
37
  RelationshipPeerData,
38
38
  RelationshipUpdatePropertyQuery,
39
39
  )
40
40
  from infrahub.core.timestamp import Timestamp
41
- from infrahub.core.utils import update_relationships_to
42
- from infrahub.exceptions import Error, NodeNotFoundError, ValidationError
41
+ from infrahub.exceptions import NodeNotFoundError, ValidationError
43
42
 
44
43
  if TYPE_CHECKING:
45
44
  from uuid import UUID
@@ -71,7 +70,6 @@ class RelationshipCreateData(BaseModel):
71
70
  direction: str
72
71
  status: str
73
72
  is_protected: bool
74
- is_visible: bool
75
73
  hierarchical: str | None = None
76
74
  source_prop: list[NodePropertyData] = Field(default_factory=list)
77
75
  owner_prop: list[NodePropertyData] = Field(default_factory=list)
@@ -85,16 +83,37 @@ class RelationshipUpdateDetails:
85
83
  peer_ids_present_database_only: list[str]
86
84
 
87
85
 
88
- class Relationship(FlagPropertyMixin, NodePropertyMixin):
86
+ @dataclass
87
+ class PeerWithRelationshipMetadata:
88
+ peer: Node | str
89
+ created_at: Timestamp | None = None
90
+ created_by: str | None = None
91
+ updated_at: Timestamp | None = None
92
+ updated_by: str | None = None
93
+ owner_id: str | None = None
94
+ source_id: str | None = None
95
+ is_protected: bool | None = None
96
+
97
+
98
+ def _use_global_branch(schema: MainSchemaTypes) -> bool:
99
+ # only use the global branch alone if the schema is branch agnostic and it is a node schema
100
+ # if the schema is a generic, then its used_by schemas could be branch-aware
101
+ return bool(schema.branch is BranchSupportType.AGNOSTIC and schema.is_node_schema)
102
+
103
+
104
+ class Relationship(FlagPropertyMixin, NodePropertyMixin, MetadataInterface):
89
105
  rel_type: str = "IS_RELATED"
90
106
 
91
107
  def __init__(
92
108
  self,
93
109
  schema: RelationshipSchema,
94
110
  branch: Branch,
111
+ source_kind: str,
95
112
  at: Timestamp | None = None,
96
113
  node: Node | None = None,
97
114
  node_id: str | None = None,
115
+ is_from_profile: bool = False,
116
+ profile_id: UUID | None = None,
98
117
  **kwargs: Any,
99
118
  ) -> None:
100
119
  if not node and not node_id:
@@ -104,6 +123,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
104
123
  self.name = schema.name
105
124
 
106
125
  self.branch = branch
126
+ self.source_kind = source_kind
107
127
  self.at = Timestamp(at)
108
128
 
109
129
  self._node = node
@@ -111,14 +131,21 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
111
131
 
112
132
  self.id: UUID | None = None
113
133
  self.db_id: str | None = None
114
- self.updated_at: Timestamp | None = None
134
+ self.is_from_profile: bool = is_from_profile
135
+ self.profile_id: UUID | None = profile_id
115
136
 
116
137
  self._peer: Node | str | None = None
117
138
  self.peer_id: str | None = None
118
139
  self.peer_hfid: list[str] | None = None
119
- self.data: dict | RelationshipPeerData | str | None = None
140
+ self.data: dict | RelationshipPeerData | str | Node | None = None
120
141
 
121
142
  self.from_pool: dict[str, Any] | None = None
143
+ self._metadata = MetadataInfo(
144
+ created_at=kwargs.get("created_at"),
145
+ created_by=kwargs.get("created_by"),
146
+ updated_at=kwargs.get("updated_at"),
147
+ updated_by=kwargs.get("updated_by"),
148
+ )
122
149
 
123
150
  self._init_node_property_mixin(kwargs=kwargs)
124
151
  self._init_flag_property_mixin(kwargs=kwargs)
@@ -166,44 +193,102 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
166
193
  return registry.get_global_branch()
167
194
  return self.branch
168
195
 
169
- def _process_data(self, data: dict | RelationshipPeerData | str) -> None:
170
- self.data = data
196
+ def _set_created_at(self, value: Timestamp | None) -> None:
197
+ self._metadata.created_at = value
171
198
 
172
- if isinstance(data, RelationshipPeerData):
173
- self.set_peer(value=str(data.peer_id))
199
+ def _set_created_by(self, value: str | None) -> None:
200
+ self._metadata.created_by = value
174
201
 
175
- if not self.id and data.rel_node_id:
176
- self.id = data.rel_node_id
177
- if not self.db_id and data.rel_node_db_id:
178
- self.db_id = data.rel_node_db_id
202
+ def _set_updated_at(self, value: Timestamp | None) -> None:
203
+ self._metadata.updated_at = value
179
204
 
180
- # Extract the properties
181
- for prop_name, prop in data.properties.items():
182
- if hasattr(self, "_flag_properties") and prop_name in self._flag_properties:
183
- setattr(self, prop_name, prop.value)
184
- elif hasattr(self, "_node_properties") and prop_name in self._node_properties:
185
- setattr(self, prop_name, prop.value)
205
+ def _set_updated_by(self, value: str | None) -> None:
206
+ self._metadata.updated_by = value
186
207
 
187
- elif isinstance(data, dict):
188
- for key, value in data.items():
189
- if key in ["peer", "id"]:
190
- self.set_peer(value=data.get(key, None))
191
- elif key == "hfid" and self.peer_id is None:
192
- self.peer_hfid = value
193
- elif key.startswith(PREFIX_PROPERTY) and key.replace(PREFIX_PROPERTY, "") in self._flag_properties:
194
- setattr(self, key.replace(PREFIX_PROPERTY, ""), value)
195
- elif key.startswith(PREFIX_PROPERTY) and key.replace(PREFIX_PROPERTY, "") in self._node_properties:
196
- setattr(self, key.replace(PREFIX_PROPERTY, ""), value)
197
- elif key == "from_pool":
198
- self.from_pool = value
208
+ def _get_created_at(self) -> Timestamp | None:
209
+ return self._metadata.created_at
210
+
211
+ def _get_created_by(self) -> str | None:
212
+ return self._metadata.created_by
199
213
 
214
+ def _get_updated_at(self) -> Timestamp | None:
215
+ return self._metadata.updated_at
216
+
217
+ def _get_updated_by(self) -> str | None:
218
+ return self._metadata.updated_by
219
+
220
+ def _process_relationship_peer_data(self, data: RelationshipPeerData) -> None:
221
+ self.set_peer(value=str(data.peer_id))
222
+
223
+ if not self.id and data.rel_node_id:
224
+ self.id = data.rel_node_id
225
+ if not self.db_id and data.rel_node_db_id:
226
+ self.db_id = data.rel_node_db_id
227
+
228
+ # Extract the properties
229
+ for prop_name, prop in data.properties.items():
230
+ if hasattr(self, "_flag_properties") and prop_name in self._flag_properties:
231
+ setattr(self, prop_name, prop.value)
232
+ elif hasattr(self, "_node_properties") and prop_name in self._node_properties:
233
+ setattr(self, prop_name, prop.value)
234
+ self._set_created_at(data.created_at)
235
+ self._set_created_by(data.created_by)
236
+ self._set_updated_at(data.updated_at)
237
+ self._set_updated_by(data.updated_by)
238
+
239
+ def _process_dict_data(self, data: dict) -> None:
240
+ for key, value in data.items():
241
+ if key in ["peer", "id"]:
242
+ self.set_peer(value=value)
243
+ elif key == "hfid" and self.peer_id is None:
244
+ self.peer_hfid = value
245
+ elif key.startswith(PREFIX_PROPERTY) and key.replace(PREFIX_PROPERTY, "") in self._flag_properties:
246
+ setattr(self, key.replace(PREFIX_PROPERTY, ""), value)
247
+ elif key.startswith(PREFIX_PROPERTY) and key.replace(PREFIX_PROPERTY, "") in self._node_properties:
248
+ setattr(self, key.replace(PREFIX_PROPERTY, ""), value)
249
+ elif key == "from_pool":
250
+ self.from_pool = value
251
+ elif key == "created_at" and value:
252
+ self._set_created_at(Timestamp(value))
253
+ elif key == "created_by" and value:
254
+ self._set_created_by(value)
255
+ elif key == "updated_at" and value:
256
+ self._set_updated_at(Timestamp(value))
257
+ elif key == "updated_by" and value:
258
+ self._set_updated_by(value)
259
+
260
+ def _process_peer_with_relationship_metadata(self, data: PeerWithRelationshipMetadata) -> None:
261
+ self.set_peer(value=data.peer)
262
+ self._set_created_at(data.created_at)
263
+ self._set_created_by(data.created_by)
264
+ self._set_updated_at(data.updated_at)
265
+ self._set_updated_by(data.updated_by)
266
+ if data.is_protected is not None:
267
+ self.is_protected = data.is_protected
268
+ if data.owner_id is not None:
269
+ self.set_owner(value=data.owner_id)
270
+ if data.source_id is not None:
271
+ self.set_source(value=data.source_id)
272
+
273
+ def _process_data(self, data: dict | RelationshipPeerData | str | Node | PeerWithRelationshipMetadata) -> None:
274
+ if isinstance(data, PeerWithRelationshipMetadata):
275
+ self.data = data.peer
276
+ else:
277
+ self.data = data
278
+
279
+ if isinstance(data, RelationshipPeerData):
280
+ self._process_relationship_peer_data(data=data)
281
+ elif isinstance(data, dict):
282
+ self._process_dict_data(data=data)
283
+ elif isinstance(data, PeerWithRelationshipMetadata):
284
+ self._process_peer_with_relationship_metadata(data=data)
200
285
  else:
201
286
  self.set_peer(value=data)
202
287
 
203
288
  async def new(
204
289
  self,
205
290
  db: InfrahubDatabase, # noqa: ARG002
206
- data: dict | RelationshipPeerData | Any = None,
291
+ data: dict | RelationshipPeerData | PeerWithRelationshipMetadata | Any = None,
207
292
  **kwargs: Any, # noqa: ARG002
208
293
  ) -> Relationship:
209
294
  self._process_data(data=data)
@@ -215,19 +300,11 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
215
300
  db: InfrahubDatabase, # noqa: ARG002
216
301
  id: UUID | None = None,
217
302
  db_id: str | None = None,
218
- updated_at: Timestamp | str | None = None,
219
303
  data: dict | RelationshipPeerData | Any = None,
220
304
  ) -> Self:
221
- hash_before = hash(self)
222
-
223
305
  self.id = id or self.id
224
306
  self.db_id = db_id or self.db_id
225
-
226
307
  self._process_data(data=data)
227
-
228
- if updated_at and hash(self) != hash_before:
229
- self.updated_at = Timestamp(updated_at)
230
-
231
308
  return self
232
309
 
233
310
  def get_kind(self) -> str:
@@ -245,8 +322,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
245
322
  kind=self.schema.kind.value,
246
323
  branch=self.branch,
247
324
  at=self.at,
248
- include_owner=True,
249
- include_source=True,
325
+ include_metadata=MetadataOptions.LINKED_NODES,
250
326
  )
251
327
  self._node = node
252
328
  self._node_id = self._node.id
@@ -290,8 +366,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
290
366
  raise_on_error=True,
291
367
  at=self.at,
292
368
  branch=self.branch,
293
- include_source=True,
294
- include_owner=True,
369
+ include_metadata=MetadataOptions.LINKED_NODES,
295
370
  prefetch_relationships=False,
296
371
  branch_agnostic=self.schema.branch is BranchSupportType.AGNOSTIC,
297
372
  )
@@ -302,8 +377,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
302
377
  kind=self.schema.peer,
303
378
  branch=self.branch,
304
379
  at=self.at,
305
- include_owner=True,
306
- include_source=True,
380
+ include_metadata=MetadataOptions.LINKED_NODES,
307
381
  branch_agnostic=self.schema.branch is BranchSupportType.AGNOSTIC,
308
382
  )
309
383
  except NodeNotFoundError:
@@ -332,15 +406,19 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
332
406
  memory_value = getattr(self, f"{property_name}_id", None)
333
407
  database_prop = data.properties.get(property_name)
334
408
  database_value = database_prop.value if database_prop else None
335
- if memory_value != database_value:
409
+ if memory_value != database_value or self.is_clear(property_name):
336
410
  different_properties.append(property_name)
337
411
 
338
412
  return different_properties
339
413
 
340
- async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
414
+ async def _create(self, db: InfrahubDatabase, user_id: str, at: Timestamp | None = None) -> None:
341
415
  """Add a relationship with another object by creating a new relationship node."""
342
416
 
343
417
  create_at = Timestamp(at)
418
+ self._set_created_by(value=user_id)
419
+ self._set_created_at(value=create_at)
420
+ self._set_updated_by(value=user_id)
421
+ self._set_updated_at(value=create_at)
344
422
 
345
423
  # Assuming nothing is present in the database yet
346
424
  # Create a new Relationship node and attach each object to it
@@ -349,7 +427,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
349
427
  branch = self.get_branch_based_on_support_type()
350
428
 
351
429
  query = await RelationshipCreateQuery.init(
352
- db=db, source=node, destination=peer, rel=self, branch=branch, at=create_at
430
+ db=db, source=node, destination=peer, rel=self, branch=branch, at=create_at, user_id=user_id
353
431
  )
354
432
  await query.execute(db=db)
355
433
  result = query.get_result()
@@ -364,6 +442,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
364
442
  db: InfrahubDatabase,
365
443
  properties_to_update: list[str],
366
444
  data: RelationshipPeerData,
445
+ user_id: str = SYSTEM_USER_ID,
367
446
  at: Timestamp | None = None,
368
447
  ) -> None:
369
448
  """Update the properties of an existing relationship."""
@@ -371,16 +450,6 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
371
450
  update_at = Timestamp(at)
372
451
  branch = self.get_branch_based_on_support_type()
373
452
 
374
- rel_ids_to_update = []
375
- for prop_name, prop in data.properties.items():
376
- if prop_name in properties_to_update and prop.rel.branch == self.branch.name:
377
- rel_ids_to_update.append(prop.rel.db_id)
378
-
379
- if rel_ids_to_update:
380
- await update_relationships_to(rel_ids_to_update, to=update_at, db=db)
381
-
382
- node = await self.get_node(db=db)
383
-
384
453
  flag_properties_to_update = {}
385
454
  for prop_name in self._flag_properties:
386
455
  if prop_name not in properties_to_update:
@@ -393,8 +462,10 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
393
462
  for prop_name in self._node_properties:
394
463
  if prop_name not in properties_to_update:
395
464
  continue
396
- if value := getattr(self, f"{prop_name}_id"):
397
- node_properties_to_update[prop_name] = value
465
+ value = getattr(self, f"{prop_name}_id")
466
+ needs_clear = self.is_clear(prop_name)
467
+ if value or needs_clear:
468
+ node_properties_to_update[prop_name] = getattr(self, f"{prop_name}_id")
398
469
 
399
470
  if not flag_properties_to_update and not node_properties_to_update:
400
471
  return
@@ -402,46 +473,48 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
402
473
  query = await RelationshipUpdatePropertyQuery.init(
403
474
  db=db,
404
475
  branch=branch,
405
- source=node,
476
+ source_id=self.node_id,
406
477
  rel=self,
478
+ user_id=user_id,
407
479
  at=update_at,
408
480
  flag_properties_to_update=flag_properties_to_update,
409
481
  node_properties_to_update=node_properties_to_update,
410
- rel_node_id=data.rel_node_id,
482
+ rel_id=data.rel_node_id,
411
483
  )
412
484
  await query.execute(db=db)
413
485
 
414
- async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
486
+ async def delete(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None) -> None:
415
487
  delete_at = Timestamp(at)
416
488
 
417
- node = await self.get_node(db=db)
418
- peer = await self.get_peer(db=db)
489
+ if self.peer_id:
490
+ peer_id = self.peer_id
491
+ else:
492
+ peer = await self.get_peer(db=db)
493
+ peer_id = peer.id
419
494
 
420
495
  branch = self.get_branch_based_on_support_type()
421
496
 
422
- get_query = await RelationshipGetQuery.init(
423
- db=db, source=node, destination=peer, rel=self, branch=self.branch, at=delete_at
424
- )
425
- await get_query.execute(db=db)
426
-
427
- rel_ids_to_update = get_query.get_relationships_ids_for_branch(branch_name=branch.name)
428
- if rel_ids_to_update is None:
429
- raise Error(
430
- f"Unable to find the relationship to delete. id: {self.id}, source: {node.id}, destination: {peer.id}"
431
- )
432
-
433
- if get_query.is_already_deleted():
434
- return
435
-
436
- # when we remove a relationship we need to :
437
- # - Update the existing relationship if we are on the same branch
438
- # - Create a new rel of type DELETED in the right branch
439
-
440
- if rel_ids_to_update:
441
- await update_relationships_to(rel_ids_to_update, to=delete_at, db=db)
497
+ source_schema = db.schema.get(name=self.source_kind, branch=self.branch, duplicate=False)
498
+ if _use_global_branch(schema=source_schema):
499
+ source_branch = registry.get_global_branch()
500
+ else:
501
+ source_branch = self.branch
502
+ destination_schema = db.schema.get(name=self.schema.peer, branch=self.branch, duplicate=False)
503
+ if _use_global_branch(schema=destination_schema):
504
+ destination_branch = registry.get_global_branch()
505
+ else:
506
+ destination_branch = self.branch
442
507
 
443
508
  delete_query = await RelationshipDeleteQuery.init(
444
- db=db, rel=self, source=node, destination=peer, branch=branch, at=delete_at
509
+ db=db,
510
+ rel=self,
511
+ source_id=self.node_id,
512
+ source_branch=source_branch,
513
+ destination_id=peer_id,
514
+ destination_branch=destination_branch,
515
+ branch=branch,
516
+ user_id=user_id,
517
+ at=delete_at,
445
518
  )
446
519
  await delete_query.execute(db=db)
447
520
 
@@ -504,13 +577,13 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
504
577
  self.set_peer(value=assigned_peer)
505
578
  self.set_source(value=pool.id)
506
579
 
507
- async def save(self, db: InfrahubDatabase, at: Timestamp | None = None) -> Self:
580
+ async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, user_id: str = SYSTEM_USER_ID) -> Self:
508
581
  """Create or Update the Relationship in the database."""
509
582
 
510
583
  save_at = Timestamp(at)
511
584
 
512
585
  if not self.id:
513
- await self._create(at=save_at, db=db)
586
+ await self._create(user_id=user_id, at=save_at, db=db)
514
587
  return self
515
588
 
516
589
  return self
@@ -536,9 +609,6 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
536
609
  response = await peer.to_graphql(fields=peer_fields, db=db, related_node_ids=related_node_ids)
537
610
 
538
611
  for field_name in rel_fields.keys():
539
- if field_name == "updated_at" and self.updated_at:
540
- response[f"{PREFIX_PROPERTY}{field_name}"] = await self.updated_at.to_graphql(db=db)
541
-
542
612
  if field_name in self._node_properties:
543
613
  node_prop_getter = getattr(self, f"get_{field_name}")
544
614
  node_prop = await node_prop_getter(db=db)
@@ -550,6 +620,16 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
550
620
  )
551
621
  if field_name in self._flag_properties:
552
622
  response[f"{PREFIX_PROPERTY}{field_name}"] = getattr(self, field_name)
623
+ if field_name == "created_at":
624
+ created_at = self._get_created_at()
625
+ response[f"{PREFIX_PROPERTY}{field_name}"] = await created_at.to_graphql() if created_at else None
626
+ elif field_name == "created_by":
627
+ response[f"{PREFIX_PROPERTY}{field_name}"] = self._get_created_by()
628
+ elif field_name in ["_updated_at", "updated_at"]:
629
+ updated_at = self._get_updated_at()
630
+ response[f"{PREFIX_PROPERTY}{field_name}"] = await updated_at.to_graphql() if updated_at else None
631
+ elif field_name == "updated_by":
632
+ response[f"{PREFIX_PROPERTY}{field_name}"] = self._get_updated_by()
553
633
 
554
634
  if fields and "__typename" in fields:
555
635
  response["__typename"] = f"Related{peer.get_kind()}"
@@ -576,7 +656,6 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
576
656
  branch_support=self.schema.branch.value if self.schema.branch else None,
577
657
  hierarchical=self.schema.hierarchical,
578
658
  is_protected=self.is_protected,
579
- is_visible=self.is_visible,
580
659
  )
581
660
  if hasattr(self, "source_id") and self.source_id:
582
661
  data.source_prop.append(NodePropertyData(name="source", peer_id=str(self.source_id)))
@@ -715,6 +794,10 @@ class RelationshipValidatorList:
715
794
  self._relationships_count -= 1
716
795
  return result
717
796
 
797
+ def replace(self, rel_to_insert: Relationship, rel_to_remove: Relationship) -> None:
798
+ self._relationships.remove(rel_to_remove)
799
+ self._relationships.append(rel_to_insert)
800
+
718
801
  def remove(self, value: Relationship) -> None:
719
802
  if self.min_count and self._relationships_count - 1 < self.min_count:
720
803
  self._raise_too_few()
@@ -748,12 +831,15 @@ class RelationshipValidatorList:
748
831
 
749
832
 
750
833
  class RelationshipManager:
751
- def __init__(self, schema: RelationshipSchema, branch: Branch, at: Timestamp, node: Node) -> None:
834
+ def __init__(
835
+ self, schema: RelationshipSchema, branch: Branch, at: Timestamp, node: Node, is_from_profile: bool = False
836
+ ) -> None:
752
837
  self.schema: RelationshipSchema = schema
753
838
  self.name: str = schema.name
754
839
  self.node: Node = node
755
840
  self.branch: Branch = branch
756
841
  self.at = at
842
+ self.is_from_profile = is_from_profile
757
843
 
758
844
  # TODO Ideally this information should come from the Schema
759
845
  self.rel_class = Relationship
@@ -804,7 +890,9 @@ class RelationshipManager:
804
890
  if not isinstance(item, rm.rel_class | str | dict) and not hasattr(item, "_schema"):
805
891
  raise ValidationError({rm.name: f"Invalid data provided to form a relationship {item}"})
806
892
 
807
- rel = rm.rel_class(schema=rm.schema, branch=rm.branch, at=rm.at, node=rm.node)
893
+ rel = rm.rel_class(
894
+ schema=rm.schema, branch=rm.branch, source_kind=rm.node.get_kind(), at=rm.at, node=rm.node
895
+ )
808
896
  await rel.new(db=db, data=item)
809
897
 
810
898
  rm._relationships.append(rel)
@@ -912,8 +1000,7 @@ class RelationshipManager:
912
1000
  db: InfrahubDatabase,
913
1001
  peer_type: type[PeerType],
914
1002
  branch_agnostic: bool = ...,
915
- include_source: bool = ...,
916
- include_owner: bool = ...,
1003
+ include_metadata: MetadataQueryOptions | MetadataOptions = MetadataOptions.NONE,
917
1004
  ) -> Mapping[str, PeerType]: ...
918
1005
 
919
1006
  @overload
@@ -922,8 +1009,7 @@ class RelationshipManager:
922
1009
  db: InfrahubDatabase,
923
1010
  peer_type: None = None,
924
1011
  branch_agnostic: bool = ...,
925
- include_source: bool = ...,
926
- include_owner: bool = ...,
1012
+ include_metadata: MetadataQueryOptions | MetadataOptions = MetadataOptions.NONE,
927
1013
  ) -> Mapping[str, Node]: ...
928
1014
 
929
1015
  async def get_peers(
@@ -931,8 +1017,7 @@ class RelationshipManager:
931
1017
  db: InfrahubDatabase,
932
1018
  peer_type: type[PeerType] | None = None, # noqa: ARG002
933
1019
  branch_agnostic: bool = False,
934
- include_source: bool = False,
935
- include_owner: bool = False,
1020
+ include_metadata: MetadataQueryOptions | MetadataOptions = MetadataOptions.NONE,
936
1021
  ) -> Mapping[str, Node | PeerType]:
937
1022
  rels = await self.get_relationships(db=db, branch_agnostic=branch_agnostic)
938
1023
  peer_ids = [rel.peer_id for rel in rels if rel.peer_id]
@@ -941,8 +1026,7 @@ class RelationshipManager:
941
1026
  ids=peer_ids,
942
1027
  branch=self.branch,
943
1028
  branch_agnostic=branch_agnostic,
944
- include_source=include_source,
945
- include_owner=include_owner,
1029
+ include_metadata=include_metadata,
946
1030
  )
947
1031
  return nodes
948
1032
 
@@ -966,8 +1050,11 @@ class RelationshipManager:
966
1050
  db=db,
967
1051
  source=self.node,
968
1052
  at=at or self.at,
969
- rel=self.rel_class(schema=self.schema, branch=self.branch, node=self.node),
1053
+ rel=self.rel_class(
1054
+ schema=self.schema, branch=self.branch, source_kind=self.node.get_kind(), node=self.node
1055
+ ),
970
1056
  branch_agnostic=branch_agnostic,
1057
+ include_metadata=MetadataOptions.IS_PROTECTED | MetadataOptions.LINKED_NODES,
971
1058
  )
972
1059
  await query.execute(db=db)
973
1060
  return list(query.get_peers())
@@ -991,7 +1078,9 @@ class RelationshipManager:
991
1078
 
992
1079
  peers = await self.get_db_peers(db=db, at=at, branch_agnostic=branch_agnostic)
993
1080
 
994
- peers_database: dict = {str(peer.peer_id): peer for peer in peers}
1081
+ self.is_from_profile = bool(peers) and all(peer.is_from_profile for peer in peers)
1082
+
1083
+ peers_database = {str(peer.peer_id): peer for peer in peers}
995
1084
  peer_ids = list(peers_database.keys())
996
1085
 
997
1086
  # Calculate which peer should be added or removed
@@ -1025,8 +1114,11 @@ class RelationshipManager:
1025
1114
  Relationship(
1026
1115
  schema=self.schema,
1027
1116
  branch=self.branch,
1117
+ source_kind=self.node.get_kind(),
1028
1118
  at=at or self.at,
1029
1119
  node=self.node,
1120
+ is_from_profile=details.peers_database[peer_id].is_from_profile,
1121
+ profile_id=details.peers_database[peer_id].profile_id,
1030
1122
  ).load(db=db, data=details.peers_database[peer_id])
1031
1123
  )
1032
1124
 
@@ -1072,15 +1164,27 @@ class RelationshipManager:
1072
1164
 
1073
1165
  return self._relationships.as_list()
1074
1166
 
1167
+ async def get_relationship(self, db: InfrahubDatabase, peer_id: str) -> Relationship | None:
1168
+ for rel in await self.get_relationships(db=db):
1169
+ if rel.peer_id == peer_id:
1170
+ return rel
1171
+ return None
1172
+
1075
1173
  async def update(
1076
1174
  self,
1077
- data: list[str | Node] | dict[str, Any] | str | Node | None,
1175
+ data: list[str | Node | dict[str, Any] | PeerWithRelationshipMetadata]
1176
+ | dict[str, Any]
1177
+ | str
1178
+ | Node
1179
+ | PeerWithRelationshipMetadata
1180
+ | None,
1078
1181
  db: InfrahubDatabase,
1079
1182
  process_delete: bool = True,
1183
+ user_id: str = SYSTEM_USER_ID,
1080
1184
  ) -> bool:
1081
1185
  """Replace and Update the list of relationships with this one."""
1082
1186
  if not isinstance(data, list):
1083
- list_data: Sequence[str | Node | dict[str, Any] | None] = [data]
1187
+ list_data: Sequence[str | Node | dict[str, Any] | PeerWithRelationshipMetadata | None] = [data]
1084
1188
  else:
1085
1189
  list_data = data
1086
1190
 
@@ -1092,7 +1196,9 @@ class RelationshipManager:
1092
1196
  changed = False
1093
1197
 
1094
1198
  for item in list_data:
1095
- if not isinstance(item, self.rel_class | str | dict | type(None)) and not hasattr(item, "_schema"):
1199
+ if not isinstance(
1200
+ item, self.rel_class | str | dict | type(None) | PeerWithRelationshipMetadata
1201
+ ) and not hasattr(item, "_schema"):
1096
1202
  raise ValidationError({self.name: f"Invalid data provided to form a relationship {item}"})
1097
1203
 
1098
1204
  if hasattr(item, "_schema"):
@@ -1105,7 +1211,7 @@ class RelationshipManager:
1105
1211
  if previous_relationships:
1106
1212
  if process_delete:
1107
1213
  for rel in previous_relationships.values():
1108
- await rel.delete(db=db)
1214
+ await rel.delete(db=db, user_id=user_id)
1109
1215
  changed = True
1110
1216
  continue
1111
1217
 
@@ -1124,9 +1230,9 @@ class RelationshipManager:
1124
1230
 
1125
1231
  # If the item is not present in the previous list of relationship, we create a new one.
1126
1232
  self._relationships.append(
1127
- await self.rel_class(schema=self.schema, branch=self.branch, at=self.at, node=self.node).new(
1128
- db=db, data=item
1129
- )
1233
+ await self.rel_class(
1234
+ schema=self.schema, branch=self.branch, source_kind=self.node.get_kind(), at=self.at, node=self.node
1235
+ ).new(db=db, data=item)
1130
1236
  )
1131
1237
  changed = True
1132
1238
 
@@ -1157,9 +1263,9 @@ class RelationshipManager:
1157
1263
 
1158
1264
  # If the item ID is not present in the previous set of relationships, create a new one
1159
1265
  self._relationships.append(
1160
- await self.rel_class(schema=self.schema, branch=self.branch, at=self.at, node=self.node).new(
1161
- db=db, data=data
1162
- )
1266
+ await self.rel_class(
1267
+ schema=self.schema, branch=self.branch, source_kind=self.node.get_kind(), at=self.at, node=self.node
1268
+ ).new(db=db, data=data)
1163
1269
  )
1164
1270
 
1165
1271
  return True
@@ -1188,42 +1294,47 @@ class RelationshipManager:
1188
1294
  self,
1189
1295
  db: InfrahubDatabase,
1190
1296
  peer_data: RelationshipPeerData,
1297
+ user_id: str,
1191
1298
  at: Timestamp | None = None,
1192
1299
  ) -> None:
1193
1300
  remove_at = Timestamp(at)
1194
1301
  branch = self.get_branch_based_on_support_type()
1195
1302
 
1196
- # - Update the existing relationship if we are on the same branch
1197
- rel_ids_per_branch = peer_data.rel_ids_per_branch()
1198
-
1199
- # In which cases do we end up here and do not want to set `to` time?
1200
- if branch.name in rel_ids_per_branch:
1201
- await update_relationships_to([str(ri) for ri in rel_ids_per_branch[branch.name]], to=remove_at, db=db)
1202
-
1203
- # - Create a new rel of type DELETED if the existing relationship is on a different branch
1204
- if peer_data.rels and {r.branch for r in peer_data.rels} == {peer_data.branch}:
1205
- return
1303
+ source_schema = db.schema.get(name=self.node.get_kind(), branch=self.branch, duplicate=False)
1304
+ if _use_global_branch(schema=source_schema):
1305
+ source_branch = registry.get_global_branch()
1306
+ else:
1307
+ source_branch = self.branch
1308
+ destination_schema = db.schema.get(name=self.schema.peer, branch=self.branch, duplicate=False)
1309
+ if _use_global_branch(schema=destination_schema):
1310
+ destination_branch = registry.get_global_branch()
1311
+ else:
1312
+ destination_branch = self.branch
1206
1313
 
1207
- query = await RelationshipDataDeleteQuery.init(
1314
+ delete_query = await RelationshipDeleteQuery.init(
1208
1315
  db=db,
1209
- rel=self.rel_class,
1316
+ rel_id=peer_data.rel_node_id,
1210
1317
  schema=self.schema,
1211
- source=self.node,
1212
- data=peer_data,
1318
+ source_id=peer_data.source_id,
1319
+ destination_id=peer_data.peer_id,
1320
+ source_branch=source_branch,
1321
+ destination_branch=destination_branch,
1213
1322
  branch=branch,
1323
+ user_id=user_id,
1214
1324
  at=remove_at,
1215
1325
  )
1216
- await query.execute(db=db)
1326
+ await delete_query.execute(db=db)
1217
1327
 
1218
1328
  async def save(
1219
- self, db: InfrahubDatabase, at: Timestamp | None = None
1329
+ self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None
1220
1330
  ) -> RelationshipCardinalityManyChangelog | RelationshipCardinalityOneChangelog:
1221
1331
  """Create or Update the Relationship in the database."""
1222
1332
 
1223
1333
  await self.resolve(db=db)
1334
+ branch_agnostic = self.schema.branch is BranchSupportType.AGNOSTIC
1224
1335
 
1225
1336
  save_at = Timestamp(at)
1226
- details = await self.fetch_relationship_ids(db=db, force_refresh=True)
1337
+ details = await self.fetch_relationship_ids(db=db, branch_agnostic=branch_agnostic, force_refresh=True)
1227
1338
  relationship_mapper = ChangelogRelationshipMapper(schema=self.schema)
1228
1339
 
1229
1340
  # If we have previously fetched the relationships from the database
@@ -1231,13 +1342,13 @@ class RelationshipManager:
1231
1342
  if self.has_fetched_relationships:
1232
1343
  for peer_id in details.peer_ids_present_database_only:
1233
1344
  relationship_mapper.remove_peer(peer_data=details.peers_database[peer_id])
1234
- await self.remove_in_db(peer_data=details.peers_database[peer_id], at=save_at, db=db)
1345
+ await self.remove_in_db(db=db, peer_data=details.peers_database[peer_id], user_id=user_id, at=save_at)
1235
1346
 
1236
1347
  # Create the new relationship that are not present in the database
1237
1348
  # and Compare the existing one
1238
- for rel in await self.get_relationships(db=db):
1349
+ for rel in await self.get_relationships(db=db, branch_agnostic=branch_agnostic):
1239
1350
  if rel.peer_id in details.peer_ids_present_local_only:
1240
- await rel.save(at=save_at, db=db)
1351
+ await rel.save(db=db, user_id=user_id, at=save_at)
1241
1352
 
1242
1353
  relationship_mapper.add_peer_from_relationship(relationship=rel)
1243
1354
 
@@ -1247,6 +1358,7 @@ class RelationshipManager:
1247
1358
  ):
1248
1359
  await rel.update(
1249
1360
  at=save_at,
1361
+ user_id=user_id,
1250
1362
  properties_to_update=properties_not_matching,
1251
1363
  data=details.peers_database[rel.peer_id],
1252
1364
  db=db,
@@ -1302,3 +1414,45 @@ class RelationshipManager:
1302
1414
 
1303
1415
  if self.name == "children" and not schema.children: # type: ignore[union-attr]
1304
1416
  raise ValidationError({self.name: f"Not supported to assign some children for {schema.kind}"})
1417
+
1418
+ async def update_relationships(
1419
+ self,
1420
+ db: InfrahubDatabase,
1421
+ relationships_to_remove: list[Relationship],
1422
+ relationships_to_insert: list[Relationship],
1423
+ ) -> bool:
1424
+ is_changed = False
1425
+ len_rels_to_remove = len(relationships_to_remove)
1426
+ len_rels_to_insert = len(relationships_to_insert)
1427
+
1428
+ if len_rels_to_remove and not len_rels_to_insert:
1429
+ for rel_to_remove in relationships_to_remove:
1430
+ if rel_to_remove.peer_id:
1431
+ await self.remove_locally(peer_id=rel_to_remove.peer_id, db=db)
1432
+ is_changed = True
1433
+ elif len_rels_to_insert and not len_rels_to_remove:
1434
+ for rel_to_insert in relationships_to_insert:
1435
+ self._relationships.append(rel_to_insert)
1436
+ is_changed = True
1437
+ elif len_rels_to_remove == len_rels_to_insert:
1438
+ for rel_to_remove, rel_to_insert in zip(relationships_to_remove, relationships_to_insert, strict=True):
1439
+ self._relationships.replace(rel_to_insert=rel_to_insert, rel_to_remove=rel_to_remove)
1440
+ is_changed = True
1441
+ elif len_rels_to_remove > len_rels_to_insert:
1442
+ for rel_to_remove, rel_to_insert in zip(
1443
+ relationships_to_remove[:len_rels_to_insert], relationships_to_insert, strict=True
1444
+ ):
1445
+ self._relationships.replace(rel_to_insert=rel_to_insert, rel_to_remove=rel_to_remove)
1446
+ for rel_to_remove in relationships_to_remove[len_rels_to_insert:]:
1447
+ if rel_to_remove.peer_id:
1448
+ await self.remove_locally(peer_id=rel_to_remove.peer_id, db=db)
1449
+ is_changed = True
1450
+ elif len_rels_to_insert > len_rels_to_remove:
1451
+ for rel_to_insert, rel_to_remove in zip(
1452
+ relationships_to_insert[:len_rels_to_remove], relationships_to_remove, strict=True
1453
+ ):
1454
+ self._relationships.replace(rel_to_insert=rel_to_insert, rel_to_remove=rel_to_remove)
1455
+ for rel_to_insert in relationships_to_insert[len_rels_to_remove:]:
1456
+ self._relationships.append(rel_to_insert)
1457
+ is_changed = True
1458
+ return is_changed