infrahub-server 1.6.2__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 (253) 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 +12 -9
  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 +26 -6
  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 +11 -6
  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_parameters.py +28 -1
  104. infrahub/core/schema/attribute_schema.py +11 -17
  105. infrahub/core/schema/basenode_schema.py +3 -0
  106. infrahub/core/schema/definitions/core/__init__.py +8 -2
  107. infrahub/core/schema/definitions/core/account.py +10 -10
  108. infrahub/core/schema/definitions/core/artifact.py +14 -8
  109. infrahub/core/schema/definitions/core/check.py +10 -4
  110. infrahub/core/schema/definitions/core/generator.py +26 -6
  111. infrahub/core/schema/definitions/core/graphql_query.py +1 -1
  112. infrahub/core/schema/definitions/core/group.py +9 -2
  113. infrahub/core/schema/definitions/core/ipam.py +80 -10
  114. infrahub/core/schema/definitions/core/menu.py +41 -7
  115. infrahub/core/schema/definitions/core/permission.py +16 -2
  116. infrahub/core/schema/definitions/core/profile.py +16 -2
  117. infrahub/core/schema/definitions/core/propose_change.py +24 -4
  118. infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
  119. infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
  120. infrahub/core/schema/definitions/core/repository.py +10 -0
  121. infrahub/core/schema/definitions/core/resource_pool.py +8 -1
  122. infrahub/core/schema/definitions/core/template.py +19 -2
  123. infrahub/core/schema/definitions/core/transform.py +11 -5
  124. infrahub/core/schema/definitions/core/webhook.py +27 -9
  125. infrahub/core/schema/manager.py +63 -43
  126. infrahub/core/schema/relationship_schema.py +6 -2
  127. infrahub/core/schema/schema_branch.py +115 -11
  128. infrahub/core/task/task.py +4 -2
  129. infrahub/core/utils.py +3 -25
  130. infrahub/core/validators/aggregated_checker.py +1 -1
  131. infrahub/core/validators/attribute/choices.py +1 -1
  132. infrahub/core/validators/attribute/enum.py +1 -1
  133. infrahub/core/validators/attribute/kind.py +6 -3
  134. infrahub/core/validators/attribute/length.py +1 -1
  135. infrahub/core/validators/attribute/min_max.py +1 -1
  136. infrahub/core/validators/attribute/number_pool.py +1 -1
  137. infrahub/core/validators/attribute/optional.py +1 -1
  138. infrahub/core/validators/attribute/regex.py +1 -1
  139. infrahub/core/validators/determiner.py +3 -3
  140. infrahub/core/validators/node/attribute.py +1 -1
  141. infrahub/core/validators/node/relationship.py +1 -1
  142. infrahub/core/validators/relationship/peer.py +1 -1
  143. infrahub/database/__init__.py +4 -4
  144. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  145. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  146. infrahub/dependencies/registry.py +2 -0
  147. infrahub/display_labels/tasks.py +12 -3
  148. infrahub/git/integrator.py +18 -18
  149. infrahub/git/tasks.py +1 -1
  150. infrahub/git/utils.py +1 -1
  151. infrahub/graphql/app.py +2 -2
  152. infrahub/graphql/constants.py +3 -0
  153. infrahub/graphql/context.py +1 -1
  154. infrahub/graphql/field_extractor.py +1 -1
  155. infrahub/graphql/initialization.py +11 -0
  156. infrahub/graphql/loaders/account.py +134 -0
  157. infrahub/graphql/loaders/node.py +5 -12
  158. infrahub/graphql/loaders/peers.py +5 -7
  159. infrahub/graphql/manager.py +175 -21
  160. infrahub/graphql/metadata.py +91 -0
  161. infrahub/graphql/mutations/account.py +6 -6
  162. infrahub/graphql/mutations/attribute.py +0 -2
  163. infrahub/graphql/mutations/branch.py +9 -5
  164. infrahub/graphql/mutations/computed_attribute.py +1 -1
  165. infrahub/graphql/mutations/display_label.py +1 -1
  166. infrahub/graphql/mutations/hfid.py +1 -1
  167. infrahub/graphql/mutations/ipam.py +4 -6
  168. infrahub/graphql/mutations/main.py +9 -4
  169. infrahub/graphql/mutations/profile.py +16 -22
  170. infrahub/graphql/mutations/proposed_change.py +4 -4
  171. infrahub/graphql/mutations/relationship.py +40 -10
  172. infrahub/graphql/mutations/repository.py +14 -12
  173. infrahub/graphql/mutations/schema.py +2 -2
  174. infrahub/graphql/order.py +14 -0
  175. infrahub/graphql/queries/branch.py +62 -6
  176. infrahub/graphql/queries/diff/tree.py +5 -5
  177. infrahub/graphql/queries/resource_manager.py +25 -24
  178. infrahub/graphql/resolvers/account_metadata.py +84 -0
  179. infrahub/graphql/resolvers/ipam.py +6 -8
  180. infrahub/graphql/resolvers/many_relationship.py +77 -35
  181. infrahub/graphql/resolvers/resolver.py +59 -14
  182. infrahub/graphql/resolvers/single_relationship.py +87 -23
  183. infrahub/graphql/subscription/graphql_query.py +2 -0
  184. infrahub/graphql/types/__init__.py +0 -1
  185. infrahub/graphql/types/attribute.py +10 -5
  186. infrahub/graphql/types/branch.py +40 -53
  187. infrahub/graphql/types/enums.py +3 -0
  188. infrahub/graphql/types/metadata.py +28 -0
  189. infrahub/graphql/types/node.py +22 -2
  190. infrahub/graphql/types/relationship.py +10 -2
  191. infrahub/graphql/types/standard_node.py +12 -7
  192. infrahub/hfid/tasks.py +12 -3
  193. infrahub/lock.py +7 -0
  194. infrahub/menu/repository.py +1 -1
  195. infrahub/patch/queries/base.py +1 -1
  196. infrahub/pools/number.py +1 -8
  197. infrahub/profiles/gather.py +56 -0
  198. infrahub/profiles/mandatory_fields_checker.py +116 -0
  199. infrahub/profiles/models.py +66 -0
  200. infrahub/profiles/node_applier.py +154 -13
  201. infrahub/profiles/queries/get_profile_data.py +143 -31
  202. infrahub/profiles/tasks.py +79 -27
  203. infrahub/profiles/triggers.py +22 -0
  204. infrahub/proposed_change/action_checker.py +1 -1
  205. infrahub/proposed_change/tasks.py +4 -1
  206. infrahub/services/__init__.py +1 -1
  207. infrahub/services/adapters/cache/nats.py +1 -1
  208. infrahub/services/adapters/cache/redis.py +7 -0
  209. infrahub/tasks/artifact.py +1 -0
  210. infrahub/transformations/tasks.py +2 -2
  211. infrahub/trigger/catalogue.py +2 -0
  212. infrahub/trigger/models.py +1 -0
  213. infrahub/trigger/setup.py +3 -3
  214. infrahub/trigger/tasks.py +3 -0
  215. infrahub/validators/tasks.py +1 -0
  216. infrahub/webhook/gather.py +1 -1
  217. infrahub/webhook/models.py +1 -1
  218. infrahub/webhook/tasks.py +23 -7
  219. infrahub/workers/dependencies.py +9 -3
  220. infrahub/workers/infrahub_async.py +13 -4
  221. infrahub/workflows/catalogue.py +19 -0
  222. infrahub_sdk/analyzer.py +2 -2
  223. infrahub_sdk/branch.py +12 -39
  224. infrahub_sdk/checks.py +4 -4
  225. infrahub_sdk/client.py +36 -0
  226. infrahub_sdk/ctl/cli_commands.py +2 -1
  227. infrahub_sdk/ctl/graphql.py +15 -4
  228. infrahub_sdk/ctl/utils.py +2 -2
  229. infrahub_sdk/enums.py +6 -0
  230. infrahub_sdk/graphql/renderers.py +21 -0
  231. infrahub_sdk/graphql/utils.py +85 -0
  232. infrahub_sdk/node/attribute.py +12 -2
  233. infrahub_sdk/node/constants.py +12 -0
  234. infrahub_sdk/node/metadata.py +69 -0
  235. infrahub_sdk/node/node.py +65 -14
  236. infrahub_sdk/node/property.py +3 -0
  237. infrahub_sdk/node/related_node.py +37 -5
  238. infrahub_sdk/node/relationship.py +18 -1
  239. infrahub_sdk/operation.py +2 -2
  240. infrahub_sdk/schema/repository.py +1 -2
  241. infrahub_sdk/transforms.py +2 -2
  242. infrahub_sdk/types.py +18 -2
  243. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
  244. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +252 -231
  245. infrahub_testcontainers/container.py +3 -3
  246. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  247. infrahub_testcontainers/docker-compose.test.yml +13 -5
  248. infrahub_testcontainers/models.py +3 -3
  249. infrahub_testcontainers/performance_test.py +1 -1
  250. infrahub/graphql/models.py +0 -6
  251. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
  252. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
  253. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Sequence
4
4
 
5
+ from infrahub.core.constants import SYSTEM_USER_ID
5
6
  from infrahub.core.migrations.shared import MigrationResult
6
7
  from infrahub.log import get_logger
7
8
 
@@ -22,5 +23,5 @@ class Migration025(InternalSchemaMigration):
22
23
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
23
24
  return MigrationResult()
24
25
 
25
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
26
+ async def execute(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> MigrationResult: # noqa: ARG002
26
27
  return await validate_nulls_in_uniqueness_constraints(db=db)
@@ -4,6 +4,7 @@ import ipaddress
4
4
  from typing import TYPE_CHECKING, Sequence
5
5
 
6
6
  from infrahub.core.branch.models import Branch
7
+ from infrahub.core.constants import SYSTEM_USER_ID
7
8
  from infrahub.core.initialization import initialization
8
9
  from infrahub.core.ipam.reconciler import IpamReconciler
9
10
  from infrahub.core.manager import NodeManager
@@ -28,7 +29,7 @@ class Migration026(InternalSchemaMigration):
28
29
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
29
30
  return MigrationResult()
30
31
 
31
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
32
+ async def execute(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> MigrationResult: # noqa: ARG002
32
33
  # load schemas from database into registry
33
34
  initialize_lock()
34
35
  await initialization(db=db)
@@ -631,7 +631,6 @@ class Migration029(ArbitraryMigration):
631
631
  offset=offset,
632
632
  outbound_edge_types=[
633
633
  DatabaseEdgeType.IS_RELATED,
634
- DatabaseEdgeType.IS_VISIBLE,
635
634
  DatabaseEdgeType.IS_PROTECTED,
636
635
  DatabaseEdgeType.HAS_OWNER,
637
636
  DatabaseEdgeType.HAS_SOURCE,
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Sequence
5
5
  from infrahub import config
6
6
  from infrahub.core import registry
7
7
  from infrahub.core.branch import Branch
8
- from infrahub.core.constants import SchemaPathType
8
+ from infrahub.core.constants import SYSTEM_USER_ID, SchemaPathType
9
9
  from infrahub.core.initialization import initialization
10
10
  from infrahub.core.migrations.shared import InternalSchemaMigration, MigrationResult, SchemaMigration
11
11
  from infrahub.core.path import SchemaPath
@@ -35,7 +35,7 @@ class Migration031(InternalSchemaMigration):
35
35
  minimum_version: int = 30
36
36
  migrations: Sequence[SchemaMigration] = []
37
37
 
38
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
38
+ async def execute(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> MigrationResult: # noqa: ARG002
39
39
  """Retrieve all number attributes that have a min/max/excluded_values
40
40
  For any of these attributes, check if corresponding existing nodes are valid."""
41
41
 
@@ -4,6 +4,7 @@ import ipaddress
4
4
  from typing import TYPE_CHECKING, Sequence
5
5
 
6
6
  from infrahub.core.branch.models import Branch
7
+ from infrahub.core.constants import SYSTEM_USER_ID
7
8
  from infrahub.core.initialization import initialization
8
9
  from infrahub.core.ipam.reconciler import IpamReconciler
9
10
  from infrahub.core.manager import NodeManager
@@ -37,7 +38,7 @@ class Migration038(InternalSchemaMigration):
37
38
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
38
39
  return MigrationResult()
39
40
 
40
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
41
+ async def execute(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> MigrationResult: # noqa: ARG002
41
42
  # load schemas from database into registry
42
43
  initialize_lock()
43
44
  await initialization(db=db)
@@ -73,7 +73,7 @@ class DeleteDuplicatedRelationshipEdges(Query):
73
73
  type = QueryType.WRITE
74
74
  insert_return = False
75
75
 
76
- def __init__(self, migrated_kind_nodes_only: bool = True, **kwargs: Any):
76
+ def __init__(self, migrated_kind_nodes_only: bool = True, **kwargs: Any) -> None:
77
77
  self.migrated_kind_nodes_only = migrated_kind_nodes_only
78
78
  super().__init__(**kwargs)
79
79
 
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core.migrations.shared import MigrationResult
6
+ from infrahub.core.query import Query, QueryType
7
+
8
+ from ..shared import GraphMigration
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.database import InfrahubDatabase
12
+
13
+
14
+ class RemoveIsVisibleRelationshipQuery(Query):
15
+ name = "remove_is_visible_relationship"
16
+ type: QueryType = QueryType.WRITE
17
+ insert_return = False
18
+
19
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
20
+ query = """
21
+ MATCH ()-[rel:IS_VISIBLE]->()
22
+ CALL (rel) {
23
+ DELETE rel
24
+ } IN TRANSACTIONS
25
+ """
26
+ self.add_to_query(query)
27
+
28
+
29
+ class RemoveIsVisibleFromDiffsQuery(Query):
30
+ name = "remove_is_visible_from_diffs"
31
+ type = QueryType.WRITE
32
+ insert_return = False
33
+
34
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
35
+ query = """
36
+ MATCH (diff_prop:DiffProperty {property_type: "IS_VISIBLE"})
37
+ CALL (diff_prop) {
38
+ DETACH DELETE diff_prop
39
+ } IN TRANSACTIONS
40
+ """
41
+ self.add_to_query(query)
42
+
43
+
44
+ class Migration049(GraphMigration):
45
+ name: str = "049_remove_is_visible_relationship"
46
+ minimum_version: int = 48
47
+ queries: Sequence[type[Query]] = [RemoveIsVisibleRelationshipQuery, RemoveIsVisibleFromDiffsQuery]
48
+
49
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
50
+ return MigrationResult()
51
+
52
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
53
+ return await self.do_execute(db=db)
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core.migrations.shared import MigrationResult
6
+ from infrahub.core.query import Query, QueryType
7
+
8
+ from ..shared import GraphMigration
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.database import InfrahubDatabase
12
+
13
+
14
+ class BackfillAttributeMetadataQuery(Query):
15
+ """Backfill created_at and updated_at for Attribute vertices on default/global branches.
16
+
17
+ - created_at = from time of HAS_ATTRIBUTE edge
18
+ - updated_at = latest from or to time of any non-HAS_ATTRIBUTE edge
19
+ """
20
+
21
+ name = "backfill_attribute_metadata"
22
+ type: QueryType = QueryType.WRITE
23
+ insert_return = False
24
+
25
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
26
+ query = """
27
+ // Find all Attribute vertices on default/global branch without created_at
28
+ MATCH (:Node)-[ha:HAS_ATTRIBUTE {status: "active"}]->(attr:Attribute)
29
+ WHERE ha.branch_level = 1
30
+ AND attr.created_at IS NULL
31
+
32
+ // created_at = from time of HAS_ATTRIBUTE edge
33
+ WITH DISTINCT attr, ha.from AS created_at
34
+
35
+ // Find latest change time from all non-HAS_ATTRIBUTE edges (both from and to)
36
+ CALL (attr) {
37
+ MATCH (attr)-[e]->()
38
+ WHERE e.branch_level = 1
39
+ WITH e.from AS change_time
40
+ WHERE change_time IS NOT NULL
41
+ RETURN change_time
42
+ UNION ALL
43
+ MATCH (attr)-[e]->()
44
+ WHERE e.branch_level = 1
45
+ AND e.to IS NOT NULL
46
+ RETURN e.to AS change_time
47
+ }
48
+ WITH attr, created_at, max(change_time) AS updated_at
49
+
50
+ // Set metadata (use coalesce to fall back to created_at if no changes found)
51
+ CALL (attr, created_at, updated_at) {
52
+ SET attr.created_at = created_at
53
+ SET attr.updated_at = coalesce(updated_at, created_at)
54
+ } IN TRANSACTIONS
55
+ """
56
+ self.add_to_query(query)
57
+
58
+
59
+ class BackfillRelationshipMetadataQuery(Query):
60
+ """Backfill created_at and updated_at for Relationship vertices on default/global branches.
61
+
62
+ - created_at = earliest from time of any IS_RELATED edge
63
+ - updated_at = latest from or to time of any non-IS_RELATED edge
64
+ """
65
+
66
+ name = "backfill_relationship_metadata"
67
+ type: QueryType = QueryType.WRITE
68
+ insert_return = False
69
+
70
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
71
+ query = """
72
+ // Find all Relationship vertices on default/global branch without created_at
73
+ MATCH (:Node)-[ir:IS_RELATED]-(rel:Relationship)
74
+ WHERE ir.branch_level = 1
75
+ AND rel.created_at IS NULL
76
+ WITH DISTINCT rel
77
+
78
+ // created_at = earliest IS_RELATED edge from time
79
+ CALL (rel) {
80
+ MATCH ()-[ir:IS_RELATED]-(rel)
81
+ WHERE ir.branch_level = 1
82
+ RETURN min(ir.from) AS created_at
83
+ }
84
+
85
+ // Find latest change time from all non-IS_RELATED edges (both from and to)
86
+ CALL (rel) {
87
+ MATCH (rel)-[e:!IS_RELATED]-()
88
+ WHERE e.branch_level = 1
89
+ WITH e.from AS change_time
90
+ WHERE change_time IS NOT NULL
91
+ RETURN change_time
92
+ UNION ALL
93
+ MATCH (rel)-[e:!IS_RELATED]-()
94
+ WHERE e.branch_level = 1
95
+ AND e.to IS NOT NULL
96
+ RETURN e.to AS change_time
97
+ }
98
+ WITH rel, created_at, max(change_time) AS updated_at
99
+
100
+ // Set metadata (use coalesce to fall back to created_at if no changes found)
101
+ CALL (rel, created_at, updated_at) {
102
+ SET rel.created_at = created_at
103
+ SET rel.updated_at = coalesce(updated_at, created_at)
104
+ } IN TRANSACTIONS
105
+ """
106
+ self.add_to_query(query)
107
+
108
+
109
+ class BackfillNodeMetadataQuery(Query):
110
+ """Backfill created_at and updated_at for Node vertices on default/global branches.
111
+
112
+ - created_at = earliest from time of any IS_PART_OF edge for Nodes with this UUID
113
+ - updated_at = latest updated_at from any linked Attribute or Relationship
114
+
115
+ Must run after BackfillAttributeMetadataQuery and BackfillRelationshipMetadataQuery
116
+ since Node.updated_at depends on field updated_at values.
117
+ """
118
+
119
+ name = "backfill_node_metadata"
120
+ type: QueryType = QueryType.WRITE
121
+ insert_return = False
122
+
123
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
124
+ query = """
125
+ // Find all Node vertices on default/global branch without created_at
126
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
127
+ WHERE e.branch_level = 1
128
+ AND n.created_at IS NULL
129
+ WITH DISTINCT n
130
+
131
+ // created_at = earliest IS_PART_OF edge from time for this UUID
132
+ // (handles migrated kind/namespace where multiple Node vertices share same UUID)
133
+ CALL (n) {
134
+ MATCH (:Node {uuid: n.uuid})-[ip:IS_PART_OF]->(:Root)
135
+ WHERE ip.branch_level = 1
136
+ RETURN min(ip.from) AS created_at
137
+ }
138
+
139
+ // updated_at = latest updated_at from any linked Attribute or Relationship
140
+ CALL (n) {
141
+ MATCH (n)-[:HAS_ATTRIBUTE|IS_RELATED]-(field:Attribute|Relationship)
142
+ WHERE field.updated_at IS NOT NULL
143
+ RETURN max(field.updated_at) AS latest_field_update
144
+ }
145
+
146
+ // Set metadata (use coalesce to fall back to created_at if no field updates found)
147
+ CALL (n, created_at, latest_field_update) {
148
+ SET n.created_at = created_at
149
+ SET n.updated_at = coalesce(latest_field_update, created_at)
150
+ } IN TRANSACTIONS
151
+ """
152
+ self.add_to_query(query)
153
+
154
+
155
+ class Migration050(GraphMigration):
156
+ name: str = "050_backfill_vertex_metadata"
157
+ minimum_version: int = 49
158
+ queries: Sequence[type[Query]] = [
159
+ BackfillAttributeMetadataQuery, # Run first
160
+ BackfillRelationshipMetadataQuery, # Run second
161
+ BackfillNodeMetadataQuery, # Run last (depends on Attr/Rel updated_at)
162
+ ]
163
+
164
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
165
+ return MigrationResult()
166
+
167
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
168
+ return await self.do_execute(db=db)
@@ -20,7 +20,7 @@ class MigrationQuery(MigrationBaseQuery):
20
20
  self,
21
21
  migration: SchemaMigration,
22
22
  **kwargs: Any,
23
- ):
23
+ ) -> None:
24
24
  self.migration = migration
25
25
  super().__init__(**kwargs)
26
26
 
@@ -32,6 +32,6 @@ class AttributeMigrationQuery(MigrationBaseQuery):
32
32
  self,
33
33
  migration: AttributeSchemaMigration,
34
34
  **kwargs: Any,
35
- ):
35
+ ) -> None:
36
36
  self.migration = migration
37
37
  super().__init__(**kwargs)
@@ -47,15 +47,20 @@ class AttributeAddQuery(Query):
47
47
  else:
48
48
  self.params["attr_value"] = NULL_VALUE
49
49
 
50
+ self.params["user_id"] = self.user_id
51
+
50
52
  self.params["rel_props"] = {
51
53
  "branch": self.branch.name,
52
54
  "branch_level": self.branch.hierarchy_level,
53
55
  "status": RelationshipStatus.ACTIVE.value,
54
56
  "from": self.at.to_string(),
57
+ "from_user_id": self.user_id,
55
58
  }
56
59
 
57
60
  self.params["is_protected_default"] = False
58
- self.params["is_visible_default"] = True
61
+
62
+ # Set metadata for vertex properties on default/global branch
63
+ self.params["set_metadata"] = self.branch.is_default or self.branch.is_global
59
64
 
60
65
  attr_value_label = GraphAttributeValueNode.get_default_label()
61
66
  if not is_large_attribute_type(self.attribute_kind):
@@ -84,8 +89,7 @@ class AttributeAddQuery(Query):
84
89
  query = """
85
90
  %(match_query)s
86
91
  MERGE (is_protected_value:Boolean { value: $is_protected_default })
87
- MERGE (is_visible_value:Boolean { value: $is_visible_default })
88
- WITH av, is_protected_value, is_visible_value
92
+ WITH av, is_protected_value
89
93
  MATCH (n:%(node_kinds_str)s)
90
94
  CALL (n) {
91
95
  MATCH (:Root)<-[r:IS_PART_OF]-(n)
@@ -98,16 +102,23 @@ class AttributeAddQuery(Query):
98
102
  ORDER BY has_attr_e.branch_level DESC, has_attr_e.from ASC, is_part_of_e.branch_level DESC, is_part_of_e.from ASC
99
103
  LIMIT 1
100
104
  }
101
- WITH n, is_part_of_e, has_attr_e, av, is_protected_value, is_visible_value
105
+ WITH n, is_part_of_e, has_attr_e, av, is_protected_value
102
106
  WHERE is_part_of_e.status = "active" AND (has_attr_e IS NULL OR has_attr_e.status = "deleted")
103
107
  CREATE (a:Attribute { name: $attr_name, branch_support: $branch_support })
104
108
  CREATE (n)-[:HAS_ATTRIBUTE $rel_props ]->(a)
105
109
  CREATE (a)-[:HAS_VALUE $rel_props ]->(av)
106
110
  CREATE (a)-[:IS_PROTECTED $rel_props]->(is_protected_value)
107
- CREATE (a)-[:IS_VISIBLE $rel_props]->(is_visible_value)
108
111
  %(uuid_generation)s
112
+ // Set metadata on Attribute and Node vertices if on default/global branch
113
+ WITH a, n, has_attr_e
114
+ CALL (a, n) {
115
+ WITH a, n
116
+ WHERE $set_metadata
117
+ SET a.created_at = $current_time, a.created_by = $user_id, a.updated_at = $current_time, a.updated_by = $user_id
118
+ SET n.updated_at = $current_time, n.updated_by = $user_id
119
+ }
109
120
  FOREACH (i in CASE WHEN has_attr_e.status = "deleted" THEN [1] ELSE [] END |
110
- SET has_attr_e.to = $current_time
121
+ SET has_attr_e.to = $current_time, has_attr_e.to_user_id = $user_id
111
122
  )
112
123
  """ % {
113
124
  "match_query": match_query,
@@ -53,13 +53,19 @@ class AttributeRemoveQuery(Query):
53
53
  self.params["current_time"] = self.at.to_string()
54
54
  self.params["branch_name"] = self.branch.name
55
55
 
56
+ self.params["user_id"] = self.user_id
57
+
56
58
  self.params["rel_props"] = {
57
59
  "branch": self.branch.name,
58
60
  "branch_level": self.branch.hierarchy_level,
59
61
  "status": RelationshipStatus.DELETED.value,
60
62
  "from": self.at.to_string(),
63
+ "from_user_id": self.user_id,
61
64
  }
62
65
 
66
+ # Set metadata for vertex properties on default/global branch
67
+ self.params["set_metadata"] = self.branch.is_default or self.branch.is_global
68
+
63
69
  def render_sub_query_per_rel_type(rel_type: str, rel_def: FieldInfo) -> str:
64
70
  subquery = [
65
71
  "WITH peer_node, rb, active_attr",
@@ -106,9 +112,9 @@ class AttributeRemoveQuery(Query):
106
112
  }
107
113
  WITH n1 as active_node, r1 as rb, attr1 as active_attr
108
114
  WHERE rb.status = "active"
109
- WITH active_attr
115
+ WITH active_node, active_attr
110
116
  MATCH (active_attr)-[]-(peer)
111
- WITH DISTINCT active_attr, peer
117
+ WITH DISTINCT active_node, active_attr, peer
112
118
  CALL (active_attr, peer) {
113
119
  MATCH (active_attr)-[r]-(peer)
114
120
  WHERE %(branch_filter)s
@@ -116,15 +122,23 @@ class AttributeRemoveQuery(Query):
116
122
  ORDER BY r.branch_level DESC, r.from DESC
117
123
  LIMIT 1
118
124
  }
119
- WITH a1 as active_attr, r1 as rb, p1 as peer_node
125
+ WITH active_node, a1 as active_attr, r1 as rb, p1 as peer_node
120
126
  WHERE rb.status = "active"
121
127
  CALL (peer_node, rb, active_attr) {
122
128
  %(sub_query_all)s
123
129
  }
124
- WITH p2 as peer_node, rb, active_attr
130
+ WITH p2 as peer_node, rb, active_node, active_attr
125
131
  FOREACH (i in CASE WHEN rb.branch = $branch_name THEN [1] ELSE [] END |
126
- SET rb.to = $current_time
132
+ SET rb.to = $current_time, rb.to_user_id = $user_id
127
133
  )
134
+ // Set metadata on Attribute and Node vertices if on default/global branch
135
+ WITH active_attr, active_node
136
+ CALL (active_attr, active_node) {
137
+ WITH active_attr, active_node
138
+ WHERE $set_metadata
139
+ SET active_attr.updated_at = $current_time, active_attr.updated_by = $user_id
140
+ SET active_node.updated_at = $current_time, active_node.updated_by = $user_id
141
+ }
128
142
  RETURN DISTINCT active_attr
129
143
  """ % {
130
144
  "branch_filter": branch_filter,
@@ -32,7 +32,6 @@ class AttributeRenameQuery(Query):
32
32
  ) -> None:
33
33
  self.previous_attr = previous_attr
34
34
  self.new_attr = new_attr
35
-
36
35
  super().__init__(**kwargs)
37
36
 
38
37
  def render_match(self) -> str:
@@ -94,11 +93,14 @@ class AttributeRenameQuery(Query):
94
93
  self.params["current_time"] = self.at.to_string()
95
94
  self.params["branch_name"] = self.branch.name
96
95
 
96
+ self.params["user_id"] = self.user_id
97
+
97
98
  self.params["rel_props_create"] = {
98
99
  "branch": self.branch.name,
99
100
  "branch_level": self.branch.hierarchy_level,
100
101
  "status": RelationshipStatus.ACTIVE.value,
101
102
  "from": self.at.to_string(),
103
+ "from_user_id": self.user_id,
102
104
  }
103
105
 
104
106
  self.params["rel_props_delete"] = {
@@ -106,8 +108,12 @@ class AttributeRenameQuery(Query):
106
108
  "branch_level": self.branch.hierarchy_level,
107
109
  "status": RelationshipStatus.DELETED.value,
108
110
  "from": self.at.to_string(),
111
+ "from_user_id": self.user_id,
109
112
  }
110
113
 
114
+ # Set metadata for vertex properties on default/global branch
115
+ self.params["set_metadata"] = self.branch.is_default or self.branch.is_global
116
+
111
117
  sub_queries_create = [
112
118
  self._render_sub_query_per_rel_type_create_new(rel_type, rel_def)
113
119
  for rel_type, rel_def in GraphAttributeRelationships.model_fields.items()
@@ -145,8 +151,9 @@ class AttributeRenameQuery(Query):
145
151
  WHERE rb.status = "active"
146
152
  CREATE (new_attr:Attribute { name: $new_attr.name, branch_support: $new_attr.branch_support })
147
153
  %(add_uuid)s
148
- WITH active_attr, new_attr
154
+ WITH active_node, active_attr, new_attr
149
155
  MATCH (active_attr)-[]-(peer)
156
+ WITH DISTINCT active_node, active_attr, new_attr, peer
150
157
  CALL (active_attr, peer) {
151
158
  MATCH (active_attr)-[r]-(peer)
152
159
  WHERE %(branch_filter)s
@@ -154,12 +161,12 @@ class AttributeRenameQuery(Query):
154
161
  ORDER BY r.branch_level DESC, r.from DESC
155
162
  LIMIT 1
156
163
  }
157
- WITH a1 as active_attr, r1 as rb, p1 as peer_node, new_attr
164
+ WITH active_node, a1 as active_attr, r1 as rb, p1 as peer_node, new_attr
158
165
  WHERE rb.status = "active"
159
166
  CALL (peer_node, rb, active_attr, new_attr){
160
167
  %(sub_query_create_all)s
161
168
  }
162
- WITH p2 as peer_node, rb, new_attr, active_attr
169
+ WITH p2 as peer_node, rb, new_attr, active_attr, active_node
163
170
  """ % {"branch_filter": branch_filter, "add_uuid": add_uuid, "sub_query_create_all": sub_query_create_all}
164
171
  self.add_to_query(query)
165
172
 
@@ -175,8 +182,17 @@ class AttributeRenameQuery(Query):
175
182
  else:
176
183
  query = """
177
184
  FOREACH (i in CASE WHEN rb.branch = $branch_name THEN [1] ELSE [] END |
178
- SET rb.to = $current_time
185
+ SET rb.to = $current_time, rb.to_user_id = $user_id
179
186
  )
187
+ WITH new_attr, active_node
188
+ // Set metadata on new Attribute and Node vertices if on default/global branch
189
+ CALL (new_attr, active_node) {
190
+ WITH new_attr, active_node
191
+ WHERE $set_metadata
192
+ SET new_attr.created_at = $current_time, new_attr.created_by = $user_id
193
+ SET new_attr.updated_at = $current_time, new_attr.updated_by = $user_id
194
+ SET active_node.updated_at = $current_time, active_node.updated_by = $user_id
195
+ }
180
196
  RETURN DISTINCT new_attr
181
197
  """
182
198
  self.add_to_query(query)
@@ -40,7 +40,6 @@ class NodeDuplicateQuery(Query):
40
40
  ) -> None:
41
41
  self.previous_node = previous_node
42
42
  self.new_node = new_node
43
-
44
43
  super().__init__(**kwargs)
45
44
 
46
45
  def render_match(self) -> str:
@@ -140,16 +139,23 @@ class NodeDuplicateQuery(Query):
140
139
  self.params["branch_level"] = self.branch.hierarchy_level
141
140
  self.params["branch_support"] = self.new_node.branch_support
142
141
 
142
+ self.params["user_id"] = self.user_id
143
+
143
144
  self.params["rel_props_new"] = {
144
145
  "status": RelationshipStatus.ACTIVE.value,
145
146
  "from": self.at.to_string(),
147
+ "from_user_id": self.user_id,
146
148
  }
147
149
 
148
150
  self.params["rel_props_prev"] = {
149
151
  "status": RelationshipStatus.DELETED.value,
150
152
  "from": self.at.to_string(),
153
+ "from_user_id": self.user_id,
151
154
  }
152
155
 
156
+ # Set metadata for vertex properties on default/global branch
157
+ self.params["set_metadata"] = self.branch.is_default or self.branch.is_global
158
+
153
159
  sub_query_out, sub_query_out_args = self._render_sub_query_out()
154
160
  sub_query_in, sub_query_in_args = self._render_sub_query_in()
155
161
 
@@ -168,6 +174,16 @@ class NodeDuplicateQuery(Query):
168
174
  WHERE rb.status = "active"
169
175
  CREATE (new_node:Node:%(labels)s { uuid: active_node.uuid, kind: $new_node.kind, namespace: $new_node.namespace, branch_support: $new_node.branch_support })
170
176
  WITH active_node, new_node
177
+ // Set metadata on new Node vertex
178
+ CALL (active_node, new_node) {
179
+ // always pass created_by/at from active node
180
+ SET new_node.created_at = active_node.created_at, new_node.created_by = active_node.created_by
181
+ WITH new_node
182
+ // set updated_by/at if we're on the default/global branch
183
+ WHERE $set_metadata
184
+ SET new_node.updated_at = $current_time, new_node.updated_by = $user_id
185
+ }
186
+
171
187
  // Process Outbound Relationship
172
188
  MATCH (active_node)-[]->(peer)
173
189
  WITH DISTINCT active_node, new_node, peer
@@ -185,7 +201,7 @@ class NodeDuplicateQuery(Query):
185
201
  }
186
202
  WITH p2 as peer_node, rel_outband, active_node, new_node
187
203
  FOREACH (i in CASE WHEN rel_outband.branch IN ["-global-", $branch] THEN [1] ELSE [] END |
188
- SET rel_outband.to = $current_time
204
+ SET rel_outband.to = $current_time, rel_outband.to_user_id = $user_id
189
205
  )
190
206
  WITH DISTINCT active_node, new_node
191
207
  // Process Inbound Relationship
@@ -205,9 +221,8 @@ class NodeDuplicateQuery(Query):
205
221
  }
206
222
  WITH p2 as peer_node, rel_inband, active_node, new_node
207
223
  FOREACH (i in CASE WHEN rel_inband.branch IN ["-global-", $branch] THEN [1] ELSE [] END |
208
- SET rel_inband.to = $current_time
224
+ SET rel_inband.to = $current_time, rel_inband.to_user_id = $user_id
209
225
  )
210
-
211
226
  RETURN DISTINCT new_node
212
227
  """ % {
213
228
  "branch_filter": branch_filter,
@@ -24,7 +24,7 @@ class SchemaAttributeUpdateQuery(Query):
24
24
  new_value: Any,
25
25
  previous_value: Any | None = None,
26
26
  **kwargs: Any,
27
- ):
27
+ ) -> None:
28
28
  self.attr_name = attribute_name
29
29
  self.node_name = node_name
30
30
  self.node_namespace = node_namespace