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
@@ -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)
@@ -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,
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
+ from infrahub.core.constants import SYSTEM_USER_ID
5
6
  from infrahub.types import is_large_attribute_type
6
7
 
7
8
  from ..query import AttributeMigrationQuery, MigrationBaseQuery
@@ -26,6 +27,11 @@ class AttributeKindUpdateMigrationQuery(AttributeMigrationQuery):
26
27
  self.params["branch_level"] = self.branch.hierarchy_level
27
28
  self.params["at"] = self.at.to_string()
28
29
  self.params["attr_name"] = self.migration.previous_attribute_schema.name
30
+ self.params["user_id"] = self.user_id
31
+
32
+ # Set metadata for vertex properties on default/global branch
33
+ self.params["set_metadata"] = self.branch.is_default or self.branch.is_global
34
+
29
35
  new_attr_value_labels = "AttributeValue"
30
36
  if needs_index:
31
37
  new_attr_value_labels += ":AttributeValueIndexed"
@@ -34,7 +40,7 @@ class AttributeKindUpdateMigrationQuery(AttributeMigrationQuery):
34
40
  // ------------
35
41
  // start with all the Attribute vertices we might care about
36
42
  // ------------
37
- MATCH (n:%(schema_kinds)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
43
+ MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
38
44
  WHERE attr.name = $attr_name
39
45
  WITH DISTINCT n, attr
40
46
 
@@ -70,7 +76,7 @@ CALL (av_is_default, av_value) {
70
76
  // ------------
71
77
  WITH 1 AS one
72
78
  LIMIT 1
73
- MATCH (n:%(schema_kinds)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
79
+ MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
74
80
  WHERE attr.name = $attr_name
75
81
  WITH DISTINCT n, attr
76
82
 
@@ -88,6 +94,7 @@ CALL (n, attr) {
88
94
  RETURN has_value_e, av
89
95
  }
90
96
 
97
+
91
98
  // ------------
92
99
  // create and update the HAS_VALUE edges
93
100
  // ------------
@@ -108,6 +115,7 @@ CALL (attr, has_value_e, av) {
108
115
  SET new_has_value_e.branch = $branch
109
116
  SET new_has_value_e.branch_level = $branch_level
110
117
  SET new_has_value_e.from = $at
118
+ SET new_has_value_e.from_user_id = $user_id
111
119
  SET new_has_value_e.to = NULL
112
120
 
113
121
  // ------------
@@ -122,6 +130,7 @@ CALL (attr, has_value_e, av) {
122
130
  SET deleted_has_value_e.branch = $branch
123
131
  SET deleted_has_value_e.branch_level = $branch_level
124
132
  SET deleted_has_value_e.from = $at
133
+ SET deleted_has_value_e.from_user_id = $user_id
125
134
  SET deleted_has_value_e.to = NULL
126
135
  }
127
136
 
@@ -132,12 +141,20 @@ CALL (attr, has_value_e, av) {
132
141
  CALL (has_value_e) {
133
142
  WITH has_value_e
134
143
  WHERE has_value_e.branch = $branch
135
- SET has_value_e.to = $at
144
+ SET has_value_e.to = $at, has_value_e.to_user_id = $user_id
145
+ }
146
+
147
+ // ------------
148
+ // Set metadata on Attribute and Node vertices if on default/global branch
149
+ // ------------
150
+ CALL (attr, n) {
151
+ WITH attr, n
152
+ WHERE $set_metadata
153
+ SET attr.updated_at = $at, attr.updated_by = $user_id
154
+ SET n.updated_at = $at, n.updated_by = $user_id
136
155
  }
137
156
  """ % {
138
- "schema_kinds": (
139
- f"{self.migration.previous_schema.kind}|Profile{self.migration.previous_schema.kind}|Template{self.migration.previous_schema.kind}"
140
- ),
157
+ "schema_kind": self.migration.previous_schema.kind,
141
158
  "branch_filter": branch_filter,
142
159
  "new_attr_value_labels": new_attr_value_labels,
143
160
  }
@@ -154,10 +171,11 @@ class AttributeKindUpdateMigration(AttributeSchemaMigration):
154
171
  branch: Branch,
155
172
  at: Timestamp | str | None = None,
156
173
  queries: Sequence[type[MigrationBaseQuery]] | None = None,
174
+ user_id: str = SYSTEM_USER_ID,
157
175
  ) -> MigrationResult:
158
176
  is_indexed_previous = is_large_attribute_type(self.previous_attribute_schema.kind)
159
177
  is_indexed_new = is_large_attribute_type(self.new_attribute_schema.kind)
160
178
  if is_indexed_previous is is_indexed_new:
161
179
  return MigrationResult()
162
180
 
163
- return await super().execute(db=db, branch=branch, at=at, queries=queries)
181
+ return await super().execute(db=db, branch=branch, at=at, queries=queries, user_id=user_id)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
+ from infrahub.core.constants import SYSTEM_USER_ID
5
6
  from infrahub.core.migrations.query.attribute_remove import AttributeRemoveQuery
6
7
  from infrahub.core.schema.generic_schema import GenericSchema
7
8
  from infrahub.core.schema.node_schema import NodeSchema
@@ -73,6 +74,7 @@ class AttributeSupportsProfileUpdateMigration(AttributeSchemaMigration):
73
74
  branch: Branch,
74
75
  at: Timestamp | str | None = None,
75
76
  queries: Sequence[type[MigrationBaseQuery]] | None = None, # noqa: ARG002
77
+ user_id: str = SYSTEM_USER_ID,
76
78
  ) -> MigrationResult:
77
79
  if (
78
80
  # no change in whether the attribute should be used on profiles
@@ -87,4 +89,4 @@ class AttributeSupportsProfileUpdateMigration(AttributeSchemaMigration):
87
89
  if not self.new_attribute_schema.support_profiles:
88
90
  profiles_queries.append(ProfilesAttributeRemoveMigrationQuery)
89
91
 
90
- return await super().execute(db=db, branch=branch, at=at, queries=profiles_queries)
92
+ return await super().execute(db=db, branch=branch, at=at, queries=profiles_queries, user_id=user_id)
@@ -3,6 +3,7 @@ from typing import Any
3
3
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer
4
4
 
5
5
  from infrahub.core.branch import Branch
6
+ from infrahub.core.constants import SYSTEM_USER_ID
6
7
  from infrahub.core.models import SchemaUpdateMigrationInfo
7
8
  from infrahub.core.path import SchemaPath
8
9
  from infrahub.core.schema.schema_branch import SchemaBranch
@@ -15,6 +16,7 @@ class SchemaApplyMigrationData(BaseModel):
15
16
  new_schema: SchemaBranch
16
17
  previous_schema: SchemaBranch
17
18
  migrations: list[SchemaUpdateMigrationInfo]
19
+ user_id: str = SYSTEM_USER_ID
18
20
 
19
21
  @model_serializer()
20
22
  def serialize_model(self) -> dict[str, Any]:
@@ -23,6 +25,7 @@ class SchemaApplyMigrationData(BaseModel):
23
25
  "previous_schema": self.previous_schema.to_dict_schema_object(),
24
26
  "new_schema": self.new_schema.to_dict_schema_object(),
25
27
  "migrations": [migration.model_dump() for migration in self.migrations],
28
+ "user_id": self.user_id,
26
29
  }
27
30
 
28
31
  @field_validator("new_schema", "previous_schema", mode="before")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from infrahub.core import registry
6
+ from infrahub.core.constants import SYSTEM_USER_ID
6
7
  from infrahub.core.node import Node
7
8
  from infrahub.core.schema.generic_schema import GenericSchema
8
9
  from infrahub.core.schema.node_schema import NodeSchema
@@ -65,10 +66,11 @@ class NodeAttributeAddMigration(AttributeSchemaMigration):
65
66
  branch: Branch,
66
67
  at: Timestamp | str | None = None,
67
68
  queries: Sequence[type[MigrationBaseQuery]] | None = None,
69
+ user_id: str = SYSTEM_USER_ID,
68
70
  ) -> MigrationResult:
69
71
  if self.new_attribute_schema.inherited is True:
70
72
  return MigrationResult()
71
- return await super().execute(db=db, branch=branch, at=at, queries=queries)
73
+ return await super().execute(db=db, branch=branch, at=at, queries=queries, user_id=user_id)
72
74
 
73
75
  async def execute_post_queries(
74
76
  self,
@@ -76,6 +78,7 @@ class NodeAttributeAddMigration(AttributeSchemaMigration):
76
78
  result: MigrationResult,
77
79
  branch: Branch,
78
80
  at: Timestamp, # noqa: ARG002
81
+ user_id: str, # noqa: ARG002
79
82
  ) -> MigrationResult:
80
83
  if self.new_attribute_schema.kind != "NumberPool":
81
84
  return result
@@ -51,12 +51,17 @@ class NodeRemoveMigrationBaseQuery(MigrationQuery):
51
51
  self.params["branch_name"] = self.branch.name
52
52
  self.params["branch"] = self.branch.name
53
53
  self.params["branch_level"] = self.branch.hierarchy_level
54
+ self.params["user_id"] = self.user_id
54
55
 
55
56
  self.params["rel_props"] = {
56
57
  "status": RelationshipStatus.DELETED.value,
57
58
  "from": self.at.to_string(),
59
+ "from_user_id": self.user_id,
58
60
  }
59
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
64
+
60
65
  node_remove_query = self.render_node_remove_query(branch_filter=branch_filter)
61
66
 
62
67
  query = """
@@ -72,6 +77,13 @@ class NodeRemoveMigrationBaseQuery(MigrationQuery):
72
77
  WITH n1 as active_node, r1 as rb
73
78
  WHERE rb.status = "active"
74
79
  %(node_remove_query)s
80
+ WITH active_node
81
+ // Set metadata on Node vertex if on default/global branch
82
+ CALL (active_node) {
83
+ WITH active_node
84
+ WHERE $set_metadata
85
+ SET active_node.updated_at = $current_time, active_node.updated_by = $user_id
86
+ }
75
87
  RETURN DISTINCT active_node
76
88
  """ % {
77
89
  "branch_filter": branch_filter,
@@ -103,12 +115,17 @@ class NodeRemoveMigrationQueryIn(NodeRemoveMigrationBaseQuery):
103
115
  }
104
116
  WITH n1 as active_node, rel_inband1 as rel_inband, p1 as peer_node
105
117
  WHERE rel_inband.status = "active"
118
+ CALL (peer_node) {
119
+ WITH peer_node
120
+ WHERE $set_metadata
121
+ SET peer_node.updated_at = $current_time, peer_node.updated_by = $user_id
122
+ }
106
123
  CALL (%(sub_query_args)s) {
107
124
  %(sub_query)s
108
125
  }
109
126
  WITH p2 as peer_node, rel_inband, active_node
110
127
  FOREACH (i in CASE WHEN rel_inband.branch IN ["-global-", $branch] THEN [1] ELSE [] END |
111
- SET rel_inband.to = $current_time
128
+ SET rel_inband.to = $current_time, rel_inband.to_user_id = $user_id
112
129
  )
113
130
  """ % {"sub_query": sub_query, "sub_query_args": sub_query_args, "branch_filter": branch_filter}
114
131
  return query
@@ -150,11 +167,16 @@ class NodeRemoveMigrationQueryOut(NodeRemoveMigrationBaseQuery):
150
167
  }
151
168
  WITH n1 as active_node, rel_outband1 as rel_outband, p1 as peer_node
152
169
  WHERE rel_outband.status = "active"
170
+ CALL (peer_node) {
171
+ WITH peer_node
172
+ WHERE $set_metadata
173
+ SET peer_node.updated_at = $current_time, peer_node.updated_by = $user_id
174
+ }
153
175
  CALL (%(sub_query_args)s) {
154
176
  %(sub_query)s
155
177
  }
156
178
  FOREACH (i in CASE WHEN rel_outband.branch IN ["-global-", $branch] THEN [1] ELSE [] END |
157
- SET rel_outband.to = $current_time
179
+ SET rel_outband.to = $current_time, rel_outband.to_user_id = $user_id
158
180
  )
159
181
  """ % {"sub_query": sub_query, "sub_query_args": sub_query_args, "branch_filter": branch_filter}
160
182