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
@@ -6,17 +6,15 @@ from typing import TYPE_CHECKING, Any, Optional, Self, Union, cast
6
6
  from pydantic import Field, field_validator
7
7
 
8
8
  from infrahub.core.branch.enums import BranchStatus
9
- from infrahub.core.constants import GLOBAL_BRANCH_NAME
9
+ from infrahub.core.constants import GLOBAL_BRANCH_NAME, SYSTEM_USER_ID
10
10
  from infrahub.core.graph import GRAPH_VERSION
11
11
  from infrahub.core.models import SchemaBranchHash # noqa: TC001
12
- from infrahub.core.node.standard import StandardNode
12
+ from infrahub.core.node.standard import StandardNode, StandardNodeOrdering
13
13
  from infrahub.core.query import Query, QueryType
14
14
  from infrahub.core.query.branch import (
15
15
  BranchNodeGetListQuery,
16
16
  DeleteBranchRelationshipsQuery,
17
- GetAllBranchInternalRelationshipQuery,
18
- RebaseBranchDeleteRelationshipQuery,
19
- RebaseBranchUpdateRelationshipQuery,
17
+ RebaseBranchQuery,
20
18
  )
21
19
  from infrahub.core.registry import registry
22
20
  from infrahub.core.timestamp import Timestamp
@@ -37,7 +35,6 @@ class Branch(StandardNode):
37
35
  origin_branch: str = "main"
38
36
  branched_from: Optional[str] = Field(default=None, validate_default=True)
39
37
  hierarchy_level: int = 2
40
- created_at: Optional[str] = Field(default=None, validate_default=True)
41
38
  is_default: bool = False
42
39
  is_global: bool = False
43
40
  is_protected: bool = False
@@ -93,11 +90,6 @@ class Branch(StandardNode):
93
90
  raise RuntimeError(f"branched_from not set for branch {self.name}")
94
91
  return self.branched_from
95
92
 
96
- @field_validator("created_at", mode="before")
97
- @classmethod
98
- def set_created_at(cls, value: str) -> str:
99
- return Timestamp(value).to_string()
100
-
101
93
  def get_created_at(self) -> str:
102
94
  if not self.created_at:
103
95
  raise RuntimeError(f"created_at not set for branch {self.name}")
@@ -162,10 +154,18 @@ class Branch(StandardNode):
162
154
  limit: int = 1000,
163
155
  ids: list[str] | None = None,
164
156
  name: str | None = None,
157
+ node_ordering: StandardNodeOrdering | None = None,
165
158
  **kwargs: Any,
166
159
  ) -> list[Self]:
160
+ node_ordering = node_ordering or StandardNodeOrdering()
167
161
  query: Query = await BranchNodeGetListQuery.init(
168
- db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs
162
+ db=db,
163
+ node_class=cls,
164
+ ids=ids,
165
+ node_name=name,
166
+ limit=limit,
167
+ node_ordering=node_ordering,
168
+ **kwargs,
169
169
  )
170
170
  await query.execute(db=db)
171
171
 
@@ -178,10 +178,18 @@ class Branch(StandardNode):
178
178
  limit: int = 1000,
179
179
  ids: list[str] | None = None,
180
180
  name: str | None = None,
181
+ partial_match: bool = False,
181
182
  **kwargs: Any,
182
183
  ) -> int:
183
184
  query: Query = await BranchNodeGetListQuery.init(
184
- db=db, node_class=cls, ids=ids, node_name=name, limit=limit, exclude_global=True, **kwargs
185
+ db=db,
186
+ node_class=cls,
187
+ ids=ids,
188
+ node_name=name,
189
+ limit=limit,
190
+ exclude_global=True,
191
+ partial_match=partial_match,
192
+ **kwargs,
185
193
  )
186
194
  return await query.count(db=db)
187
195
 
@@ -208,7 +216,7 @@ class Branch(StandardNode):
208
216
 
209
217
  return [default_branch, self.name]
210
218
 
211
- def get_branches_and_times_to_query(self, at: Optional[Union[Timestamp, str]] = None) -> dict[frozenset, str]:
219
+ def get_branches_and_times_to_query(self, at: Optional[Timestamp] = None) -> dict[frozenset, str]:
212
220
  """Return all the names of the branches that are constituing this branch with the associated times excluding the global branch"""
213
221
 
214
222
  at = Timestamp(at)
@@ -229,7 +237,7 @@ class Branch(StandardNode):
229
237
 
230
238
  def get_branches_and_times_to_query_global(
231
239
  self,
232
- at: Optional[Union[Timestamp, str]] = None,
240
+ at: Optional[Timestamp] = None,
233
241
  is_isolated: bool = True,
234
242
  ) -> dict[frozenset, str]:
235
243
  """Return all the names of the branches that are constituting this branch with the associated times."""
@@ -281,9 +289,9 @@ class Branch(StandardNode):
281
289
 
282
290
  return start, end
283
291
 
284
- async def create(self, db: InfrahubDatabase) -> bool:
292
+ async def create(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> bool:
285
293
  self.graph_version = GRAPH_VERSION
286
- return await super().create(db=db)
294
+ return await super().create(db=db, user_id=user_id)
287
295
 
288
296
  async def delete(self, db: InfrahubDatabase) -> None:
289
297
  if self.is_default:
@@ -299,7 +307,7 @@ class Branch(StandardNode):
299
307
  await super().delete(db=db)
300
308
 
301
309
  def get_query_filter_relationships(
302
- self, rel_labels: list, at: Optional[Union[Timestamp, str]] = None, include_outside_parentheses: bool = False
310
+ self, rel_labels: list, at: Optional[Timestamp] = None, include_outside_parentheses: bool = False
303
311
  ) -> tuple[list, dict]:
304
312
  """
305
313
  Generate a CYPHER Query filter based on a list of relationships to query a part of the graph at a specific time and on a specific branch.
@@ -362,7 +370,7 @@ class Branch(StandardNode):
362
370
  params[f"{pp}time1"] = at_str
363
371
  return filter_str, params
364
372
 
365
- branches_times = self.get_branches_and_times_to_query_global(at=at_str, is_isolated=is_isolated)
373
+ branches_times = self.get_branches_and_times_to_query_global(at=at, is_isolated=is_isolated)
366
374
 
367
375
  for idx, (branch_name, time_to_query) in enumerate(branches_times.items()):
368
376
  params[f"{pp}branch{idx}"] = list(branch_name)
@@ -387,8 +395,8 @@ class Branch(StandardNode):
387
395
  def get_query_filter_relationships_range(
388
396
  self,
389
397
  rel_labels: list,
390
- start_time: Union[Timestamp, str],
391
- end_time: Union[Timestamp, str],
398
+ start_time: Timestamp,
399
+ end_time: Timestamp,
392
400
  include_outside_parentheses: bool = False,
393
401
  include_global: bool = False,
394
402
  ) -> tuple[list, dict]:
@@ -468,9 +476,7 @@ class Branch(StandardNode):
468
476
 
469
477
  return filters, params
470
478
 
471
- def get_query_filter_range(
472
- self, rel_label: list, start_time: Union[Timestamp, str], end_time: Union[Timestamp, str]
473
- ) -> tuple[list, dict]:
479
+ def get_query_filter_range(self, rel_label: list, start_time: Timestamp, end_time: Timestamp) -> tuple[list, dict]:
474
480
  """
475
481
  Generate a CYPHER Query filter to query a range of values in the graph between start_time and end_time."""
476
482
 
@@ -496,67 +502,34 @@ class Branch(StandardNode):
496
502
 
497
503
  return filters, params
498
504
 
499
- async def rebase(self, db: InfrahubDatabase, at: Optional[Union[str, Timestamp]] = None) -> None:
505
+ async def rebase(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> None:
500
506
  """Rebase the current Branch with its origin branch"""
501
507
 
502
- at = Timestamp(at)
503
-
504
- # Find all relationships with the name of the branch
505
- # Delete all relationship that have a to date defined in the past
506
- # Update the from time on all other relationships
507
- # If conflict is set, ignore the one with Drop
508
+ at = Timestamp()
508
509
 
509
510
  await self.rebase_graph(db=db, at=at)
510
511
 
511
- # FIXME, we must ensure that there is no conflict before rebasing a branch
512
- # Otherwise we could endup with a complicated situation
513
512
  self.branched_from = at.to_string()
514
513
  self.status = BranchStatus.OPEN
515
- await self.save(db=db)
514
+ await self.save(db=db, user_id=user_id)
516
515
 
517
516
  # Update the branch in the registry after the rebase
518
517
  registry.branch[self.name] = self
519
518
 
520
- async def rebase_graph(self, db: InfrahubDatabase, at: Optional[Timestamp] = None) -> None:
521
- at = Timestamp(at)
519
+ async def rebase_graph(self, db: InfrahubDatabase, at: Timestamp) -> None:
520
+ """Rebase all relationships on this branch to a new point in time.
522
521
 
523
- query = await GetAllBranchInternalRelationshipQuery.init(db=db, branch=self)
524
- await query.execute(db=db)
522
+ This method updates the graph to reflect the state of the branch as if it had been created
523
+ at the specified timestamp. Relationships are processed as follows:
525
524
 
526
- rels_to_delete = []
527
- rels_to_update = []
528
- for result in query.get_results():
529
- element_id = result.get("r").element_id
525
+ - Relationships with no `to` timestamp and `from` <= at: Updated to start from `at`
526
+ - Relationships with `to` < at: Deleted (ended before rebase point)
527
+ - Relationships with `to` >= at: Updated to start from `at`
530
528
 
531
- conflict_status = result.get("r").get("conflict", None)
532
- if conflict_status and conflict_status == "drop":
533
- rels_to_delete.append(element_id)
534
- continue
535
-
536
- time_to_str = result.get("r").get("to", None)
537
- time_from_str = result.get("r").get("from")
538
- time_from = Timestamp(time_from_str)
539
-
540
- if not time_to_str and time_from_str and time_from <= at:
541
- rels_to_update.append(element_id)
542
- continue
543
-
544
- if not time_to_str and time_from_str and time_from > at:
545
- rels_to_delete.append(element_id)
546
- continue
547
-
548
- time_to = Timestamp(time_to_str)
549
- if time_to < at:
550
- rels_to_delete.append(element_id)
551
- continue
552
-
553
- rels_to_update.append(element_id)
554
-
555
- update_query = await RebaseBranchUpdateRelationshipQuery.init(db=db, ids=rels_to_update, at=at)
556
- await update_query.execute(db=db)
557
-
558
- delete_query = await RebaseBranchDeleteRelationshipQuery.init(db=db, ids=rels_to_delete, at=at)
559
- await delete_query.execute(db=db)
529
+ Orphaned nodes (nodes with no remaining relationships) are also cleaned up.
530
+ """
531
+ query = await RebaseBranchQuery.init(db=db, branch=self, at=at)
532
+ await query.execute(db=db)
560
533
 
561
534
 
562
535
  registry.branch_object = Branch
@@ -170,7 +170,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
170
170
  migrations = []
171
171
  async with lock.registry.global_graph_lock():
172
172
  async with db.start_transaction() as dbt:
173
- await obj.rebase(db=dbt)
173
+ await obj.rebase(db=dbt, user_id=context.account.account_id)
174
174
  log.info("Branch successfully rebased")
175
175
 
176
176
  if obj.has_schema_changes:
@@ -187,7 +187,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
187
187
  )
188
188
  registry.schema.set_schema_branch(name=obj.name, schema=updated_schema)
189
189
  obj.update_schema_hash()
190
- await obj.save(db=db)
190
+ await obj.save(db=db, user_id=context.account.account_id)
191
191
 
192
192
  # Execute the migrations
193
193
  migrations = await merger.calculate_migrations(target_schema=updated_schema)
@@ -198,6 +198,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
198
198
  new_schema=candidate_schema,
199
199
  previous_schema=schema_in_main_before,
200
200
  migrations=migrations,
201
+ user_id=context.account.account_id,
201
202
  )
202
203
  )
203
204
  for error in errors:
@@ -302,6 +303,7 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id
302
303
  new_schema=merger.destination_schema,
303
304
  previous_schema=merger.initial_source_schema,
304
305
  migrations=merger.migrations,
306
+ user_id=context.account.account_id,
305
307
  )
306
308
  )
307
309
  for error in errors:
@@ -431,7 +433,7 @@ async def create_branch(model: BranchCreateModel, context: InfrahubContext) -> N
431
433
  new_schema = origin_schema.duplicate(name=obj.name)
432
434
  registry.schema.set_schema_branch(name=obj.name, schema=new_schema)
433
435
  obj.update_schema_hash()
434
- await obj.save(db=db)
436
+ await obj.save(db=db, user_id=context.account.account_id)
435
437
 
436
438
  # Add Branch to registry
437
439
  registry.branch[obj.name] = obj
@@ -116,13 +116,6 @@ class DiffChangelogCollector:
116
116
  value_current=self._convert_string_boolean_value(value=attr_property.new_value),
117
117
  value_previous=self._convert_string_boolean_value(value=attr_property.previous_value),
118
118
  )
119
- case DatabaseEdgeType.IS_VISIBLE:
120
- if _keep_branch_update(diff_property=attr_property):
121
- changelog_attribute.add_property(
122
- name="is_visible",
123
- value_current=self._convert_string_boolean_value(value=attr_property.new_value),
124
- value_previous=self._convert_string_boolean_value(value=attr_property.previous_value),
125
- )
126
119
  case DatabaseEdgeType.HAS_SOURCE:
127
120
  if _keep_branch_update(diff_property=attr_property):
128
121
  changelog_attribute.add_property(
@@ -176,12 +169,6 @@ class DiffChangelogCollector:
176
169
  value_current=self._convert_string_boolean_value(value=rel_prop.new_value),
177
170
  value_previous=self._convert_string_boolean_value(value=rel_prop.previous_value),
178
171
  )
179
- case DatabaseEdgeType.IS_VISIBLE:
180
- changelog_rel.add_property(
181
- name="is_visible",
182
- value_current=self._convert_string_boolean_value(value=rel_prop.new_value),
183
- value_previous=self._convert_string_boolean_value(value=rel_prop.previous_value),
184
- )
185
172
  case DatabaseEdgeType.HAS_OWNER:
186
173
  changelog_rel.add_property(
187
174
  name="owner",
@@ -198,7 +185,7 @@ class DiffChangelogCollector:
198
185
  node.add_relationship(relationship_changelog=changelog_rel)
199
186
 
200
187
  def _convert_string_boolean_value(self, value: str | None) -> bool | None:
201
- """Convert string based boolean for is_protected and is_visible."""
188
+ """Convert string based boolean for is_protected."""
202
189
  if value is not None:
203
190
  return str_to_bool(value)
204
191
 
@@ -218,12 +205,6 @@ class DiffChangelogCollector:
218
205
  )
219
206
  for peer_prop in peer.properties:
220
207
  match peer_prop.property_type:
221
- case DatabaseEdgeType.IS_VISIBLE:
222
- peer_log.add_property(
223
- name="is_visible",
224
- value_current=self._convert_string_boolean_value(value=peer_prop.new_value),
225
- value_previous=self._convert_string_boolean_value(value=peer_prop.previous_value),
226
- )
227
208
  case DatabaseEdgeType.IS_PROTECTED:
228
209
  peer_log.add_property(
229
210
  name="is_protected",
@@ -186,9 +186,6 @@ class RelationshipCardinalityManyChangelog(BaseModel):
186
186
  properties["is_protected"] = PropertyChangelog(
187
187
  name="is_protected", value=relationship.is_protected, value_previous=None
188
188
  )
189
- properties["is_visible"] = PropertyChangelog(
190
- name="is_visible", value=relationship.is_protected, value_previous=None
191
- )
192
189
  if owner := getattr(relationship, "owner_id", None):
193
190
  properties["owner"] = PropertyChangelog(name="owner", value=owner, value_previous=None)
194
191
  if source := getattr(relationship, "source_id", None):
@@ -280,9 +277,6 @@ class NodeChangelog(BaseModel):
280
277
  changelog_relationship.add_property(
281
278
  name="is_protected", value_current=relationship.is_protected, value_previous=None
282
279
  )
283
- changelog_relationship.add_property(
284
- name="is_visible", value_current=relationship.is_visible, value_previous=None
285
- )
286
280
  self.relationships[changelog_relationship.name] = changelog_relationship
287
281
  elif relationship.schema.cardinality == RelationshipCardinality.MANY:
288
282
  if relationship.schema.name not in self.relationships:
@@ -340,7 +334,6 @@ class NodeChangelog(BaseModel):
340
334
  if owner_id := getattr(attribute, "owner_id", None):
341
335
  changelog_attribute.add_property(name="owner", value_current=owner_id, value_previous=None)
342
336
  changelog_attribute.add_property(name="is_protected", value_current=attribute.is_protected, value_previous=None)
343
- changelog_attribute.add_property(name="is_visible", value_current=attribute.is_visible, value_previous=None)
344
337
  self.attributes[changelog_attribute.name] = changelog_attribute
345
338
 
346
339
  def get_related_nodes(self) -> list[ChangelogRelatedNode]:
@@ -46,6 +46,8 @@ NULL_VALUE = "NULL"
46
46
 
47
47
  EVENT_NAMESPACE = "infrahub"
48
48
 
49
+ SYSTEM_USER_ID = "__system__"
50
+
49
51
 
50
52
  class EventType(InfrahubStringEnum):
51
53
  BRANCH_CREATED = f"{EVENT_NAMESPACE}.branch.created"
@@ -359,6 +361,21 @@ class AttributeDBNodeType(Flag):
359
361
  IPNETWORK = DEFAULT | INDEX_ONLY | IPNETWORK_ONLY
360
362
 
361
363
 
364
+ class MetadataOptions(Flag):
365
+ NONE = 0
366
+ SOURCE = auto()
367
+ OWNER = auto()
368
+ LINKED_NODES = SOURCE | OWNER
369
+ IS_PROTECTED = auto()
370
+ CREATED_BY = auto()
371
+ CREATED_AT = auto()
372
+ UPDATED_BY = auto()
373
+ UPDATED_AT = auto()
374
+ TIMESTAMPS = CREATED_AT | UPDATED_AT
375
+ USERS = CREATED_BY | UPDATED_BY
376
+ USER_TIMESTAMPS = TIMESTAMPS | USERS
377
+
378
+
362
379
  RESTRICTED_NAMESPACES: list[str] = [
363
380
  "Account",
364
381
  "Branch",
@@ -8,7 +8,6 @@ class DatabaseEdgeType(Enum):
8
8
  HAS_ATTRIBUTE = "HAS_ATTRIBUTE"
9
9
  IS_RELATED = "IS_RELATED"
10
10
  HAS_VALUE = "HAS_VALUE"
11
- IS_VISIBLE = "IS_VISIBLE"
12
11
  IS_PROTECTED = "IS_PROTECTED"
13
12
  HAS_OWNER = "HAS_OWNER"
14
13
  HAS_SOURCE = "HAS_SOURCE"
@@ -4,7 +4,6 @@ PARENT_CHILD_IDENTIFIER = "parent__child"
4
4
 
5
5
 
6
6
  class FlagProperty(Enum):
7
- IS_VISIBLE = "is_visible"
8
7
  IS_PROTECTED = "is_protected"
9
8
 
10
9
 
@@ -1,6 +1,5 @@
1
1
  from infrahub import lock
2
2
  from infrahub.core.branch import Branch
3
- from infrahub.core.constants.infrahubkind import REPOSITORYVALIDATOR, USERVALIDATOR
4
3
  from infrahub.core.convert_object_type.object_conversion import (
5
4
  ConversionFieldInput,
6
5
  convert_object_type,
@@ -8,7 +7,7 @@ from infrahub.core.convert_object_type.object_conversion import (
8
7
  )
9
8
  from infrahub.core.manager import NodeManager
10
9
  from infrahub.core.node import Node
11
- from infrahub.core.protocols import CoreReadOnlyRepository, CoreRepository
10
+ from infrahub.core.protocols import CoreReadOnlyRepository, CoreRepository, CoreRepositoryValidator, CoreUserValidator
12
11
  from infrahub.core.schema import NodeSchema
13
12
  from infrahub.core.timestamp import Timestamp
14
13
  from infrahub.database import InfrahubDatabase
@@ -35,11 +34,11 @@ async def convert_repository_type(
35
34
 
36
35
  # Fetch validators before deleting the repository otherwise validator-repository would no longer exist
37
36
  user_validators = await NodeManager.query(
38
- db=dbt, schema=USERVALIDATOR, prefetch_relationships=True, filters={"repository__id": repository.id}
37
+ db=dbt, schema=CoreUserValidator, prefetch_relationships=True, filters={"repository__id": repository.id}
39
38
  )
40
39
  repository_validators = await NodeManager.query(
41
40
  db=dbt,
42
- schema=REPOSITORYVALIDATOR,
41
+ schema=CoreRepositoryValidator,
43
42
  prefetch_relationships=True,
44
43
  filters={"repository__id": repository.id},
45
44
  )
@@ -1,10 +1,11 @@
1
1
  from enum import Enum
2
2
 
3
- from infrahub.core.constants import BranchConflictKeep, InfrahubKind
3
+ from infrahub.core.constants import BranchConflictKeep
4
4
  from infrahub.core.diff.query.filters import EnrichedDiffQueryFilters
5
5
  from infrahub.core.integrity.object_conflict.conflict_recorder import ObjectConflictValidatorRecorder
6
6
  from infrahub.core.manager import NodeManager
7
7
  from infrahub.core.node import Node
8
+ from infrahub.core.protocols import CoreProposedChange
8
9
  from infrahub.database import InfrahubDatabase
9
10
  from infrahub.exceptions import SchemaNotFoundError
10
11
  from infrahub.proposed_change.constants import ProposedChangeState
@@ -52,7 +53,7 @@ class DiffDataCheckSynchronizer:
52
53
  try:
53
54
  proposed_changes = await NodeManager.query(
54
55
  db=self.db,
55
- schema=InfrahubKind.PROPOSEDCHANGE,
56
+ schema=CoreProposedChange,
56
57
  filters={"source_branch": enriched_diff.diff_branch_name, "state": ProposedChangeState.OPEN},
57
58
  )
58
59
  except SchemaNotFoundError:
@@ -28,7 +28,7 @@ class DiffCardinalityOneEnricher(DiffEnricherInterface):
28
28
  - the peer_id property of the element will be the latest non-null peer ID for this element
29
29
  - the element MUST have an EnrichedDiffProperty of property_type=IS_RELATED that correctly records
30
30
  the previous and new values of the peer ID for this element
31
- - changes to properties (IS_VISIBLE, etc) of a cardinality=one relationship are consolidated as well
31
+ - changes to properties (IS_PROTECTED, etc) of a cardinality=one relationship are consolidated as well
32
32
  """
33
33
 
34
34
  def __init__(self, db: InfrahubDatabase):
@@ -6,6 +6,7 @@ from infrahub.core import registry
6
6
  from infrahub.core.constants import DiffAction
7
7
  from infrahub.core.diff.model.path import BranchTrackingId
8
8
  from infrahub.core.diff.query.merge import (
9
+ DiffMergeMetadataQuery,
9
10
  DiffMergeMigratedKindsQuery,
10
11
  DiffMergePropertiesQuery,
11
12
  DiffMergeQuery,
@@ -26,6 +27,8 @@ log = get_logger()
26
27
 
27
28
 
28
29
  class DiffMerger:
30
+ metadata_batch_size = 500
31
+
29
32
  def __init__(
30
33
  self,
31
34
  db: InfrahubDatabase,
@@ -39,6 +42,7 @@ class DiffMerger:
39
42
  self.db = db
40
43
  self.diff_repository = diff_repository
41
44
  self.serializer = serializer
45
+ self._affected_node_uuids: list[str] = []
42
46
 
43
47
  async def merge_graph(self, at: Timestamp) -> EnrichedDiffRoot:
44
48
  tracking_id = BranchTrackingId(name=self.source_branch.name)
@@ -69,6 +73,7 @@ class DiffMerger:
69
73
  # make sure that we use the ADDED db_id if it exists
70
74
  # it will not if a node was migrated and then deleted
71
75
  migrated_kinds_id_map[n.uuid] = n.identifier.db_id
76
+
72
77
  async for node_diff_dicts, property_diff_dicts in self.serializer.serialize_diff(diff=enriched_diff):
73
78
  if node_diff_dicts:
74
79
  log.info(f"Merging batch of nodes #{batch_num}")
@@ -105,13 +110,34 @@ class DiffMerger:
105
110
  )
106
111
  await migrated_merge_query.execute(db=self.db)
107
112
 
113
+ affected_node_uuids = [n.uuid for n in enriched_diff.nodes]
114
+ self._affected_node_uuids = affected_node_uuids
115
+ if affected_node_uuids:
116
+ for i in range(0, len(affected_node_uuids), self.metadata_batch_size):
117
+ batch_uuids = affected_node_uuids[i : i + self.metadata_batch_size]
118
+ log.info(f"Updating metadata for batch {i // self.metadata_batch_size + 1} ({len(batch_uuids)} nodes)")
119
+ metadata_query = await DiffMergeMetadataQuery.init(
120
+ db=self.db,
121
+ branch=self.source_branch,
122
+ at=at,
123
+ target_branch=self.destination_branch,
124
+ node_uuids=batch_uuids,
125
+ )
126
+ await metadata_query.execute(db=self.db)
127
+
108
128
  self.source_branch.branched_from = at.to_string()
109
129
  await self.source_branch.save(db=self.db)
110
130
  registry.branch[self.source_branch.name] = self.source_branch
111
131
  return enriched_diff
112
132
 
113
133
  async def rollback(self, at: Timestamp) -> None:
134
+ if not self._affected_node_uuids:
135
+ return
114
136
  rollback_query = await DiffMergeRollbackQuery.init(
115
- db=self.db, branch=self.source_branch, target_branch=self.destination_branch, at=at
137
+ db=self.db,
138
+ branch=self.source_branch,
139
+ target_branch=self.destination_branch,
140
+ at=at,
141
+ node_uuids=self._affected_node_uuids,
116
142
  )
117
143
  await rollback_query.execute(db=self.db)
@@ -93,9 +93,7 @@ class DiffMergeSerializer:
93
93
  if property_type in (DatabaseEdgeType.HAS_OWNER, DatabaseEdgeType.HAS_SOURCE, DatabaseEdgeType.IS_RELATED):
94
94
  return raw_value
95
95
  # these are boolean
96
- if (property_type in (DatabaseEdgeType.IS_VISIBLE, DatabaseEdgeType.IS_PROTECTED)) and isinstance(
97
- raw_value, str
98
- ):
96
+ if property_type == DatabaseEdgeType.IS_PROTECTED and isinstance(raw_value, str):
99
97
  return raw_value.lower() == "true"
100
98
  # this must be HAS_VALUE
101
99
  if raw_value in (None, NULL_VALUE):
@@ -249,13 +247,8 @@ class DiffMergeSerializer:
249
247
  return attr_dict, attr_prop_dict
250
248
 
251
249
  def _get_default_property_merge_dicts(self, action: DiffAction) -> dict[DatabaseEdgeType, PropertyMergeDict]:
252
- # start with default values for IS_VISIBLE and IS_PROTECTED b/c we always want to update them during a merge
250
+ # start with default values for IS_PROTECTED b/c we always want to update them during a merge
253
251
  return {
254
- DatabaseEdgeType.IS_VISIBLE: PropertyMergeDict(
255
- property_type=DatabaseEdgeType.IS_VISIBLE.value,
256
- action=self._to_action_str(action),
257
- value=None,
258
- ),
259
252
  DatabaseEdgeType.IS_PROTECTED: PropertyMergeDict(
260
253
  property_type=DatabaseEdgeType.IS_PROTECTED.value,
261
254
  action=self._to_action_str(action),
@@ -335,7 +328,7 @@ class DiffMergeSerializer:
335
328
  # handled above
336
329
  continue
337
330
  python_value_type: type = str
338
- if property_diff.property_type in (DatabaseEdgeType.IS_VISIBLE, DatabaseEdgeType.IS_PROTECTED):
331
+ if property_diff.property_type is DatabaseEdgeType.IS_PROTECTED:
339
332
  python_value_type = bool
340
333
  actions_and_values = self._get_property_actions_and_values(
341
334
  property_diff=property_diff, python_value_type=python_value_type
@@ -26,7 +26,7 @@ class BaseDiffElement(BaseModel):
26
26
  """
27
27
  resp: dict[str, Any] = {}
28
28
  for key, value in self:
29
- field_info = self.model_fields[key]
29
+ field_info = self.__class__.model_fields[key]
30
30
  if isinstance(value, BaseModel):
31
31
  resp[key] = value.to_graphql() # type: ignore[attr-defined]
32
32
  elif isinstance(value, dict):