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
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ from dataclasses import dataclass, field
4
5
  from enum import Enum
5
6
  from typing import TYPE_CHECKING, Any, Optional, Union, get_args, get_origin
6
7
  from uuid import UUID
7
8
 
8
9
  import ujson
9
10
  from infrahub_sdk.uuidt import UUIDT
10
- from pydantic import BaseModel
11
+ from pydantic import BaseModel, Field, field_validator
11
12
 
12
- from infrahub.core.constants import NULL_VALUE
13
+ from infrahub.constants.enums import OrderByField, OrderDirection
14
+ from infrahub.core.constants import NULL_VALUE, SYSTEM_USER_ID, InfrahubKind
13
15
  from infrahub.core.query.standard_node import (
14
16
  StandardNodeCreateQuery,
15
17
  StandardNodeDeleteQuery,
@@ -18,6 +20,7 @@ from infrahub.core.query.standard_node import (
18
20
  StandardNodeQuery,
19
21
  StandardNodeUpdateQuery,
20
22
  )
23
+ from infrahub.core.timestamp import Timestamp, current_timestamp
21
24
  from infrahub.exceptions import Error, InitializationError
22
25
 
23
26
  if TYPE_CHECKING:
@@ -29,9 +32,25 @@ if TYPE_CHECKING:
29
32
  from infrahub.database import InfrahubDatabase
30
33
 
31
34
 
35
+ @dataclass
36
+ class StandardNodeOrdering:
37
+ order_by: OrderByField = field(default=OrderByField.ID)
38
+ direction: OrderDirection = field(default=OrderDirection.ASC)
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class StandardNodeQueryFields:
43
+ node: dict[str, Any] = field(default_factory=dict)
44
+ node_metadata: dict[str, Any] = field(default_factory=dict)
45
+
46
+
32
47
  class StandardNode(BaseModel):
33
48
  id: Optional[str] = None
34
49
  uuid: Optional[UUID] = None
50
+ created_at: Optional[str] = Field(default=None, validate_default=True)
51
+ created_by: str = Field(default=SYSTEM_USER_ID)
52
+ updated_by: Optional[str] = Field(default=None)
53
+ updated_at: Optional[str] = Field(default=None, validate_default=True)
35
54
 
36
55
  _query: type[StandardNodeQuery] = StandardNodeCreateQuery
37
56
  _exclude_attrs: list[str] = ["id", "uuid", "_query"]
@@ -45,6 +64,11 @@ class StandardNode(BaseModel):
45
64
  raise ValueError("id isn't defined yet")
46
65
  return self.id
47
66
 
67
+ @field_validator("created_at", mode="before")
68
+ @classmethod
69
+ def set_created_at(cls, value: str) -> str:
70
+ return Timestamp(value).to_string()
71
+
48
72
  @staticmethod
49
73
  def guess_field_type(field: FieldInfo) -> Any:
50
74
  """Return the type of a Pydantic model field.
@@ -68,7 +92,14 @@ class StandardNode(BaseModel):
68
92
 
69
93
  raise InitializationError("The root node has not been initialized with a uuid")
70
94
 
71
- async def to_graphql(self, fields: dict) -> dict:
95
+ async def to_graphql_flat(self, fields: dict) -> dict:
96
+ """Returns the GraphQL representation of the object with only top-level fields.
97
+
98
+ This method does not handle nested fields and is only used for the old `Branch` query which
99
+ will be deprecated in the future and replaced by the `InfrahubBranch` query.
100
+ It's also used for the old style of Branch mutations that will be deprecated in the future,
101
+ when we introduce InfrahubBranch muations for consistency.
102
+ """
72
103
  response: dict[str, Any] = {"id": self.uuid}
73
104
 
74
105
  for field_name in fields.keys():
@@ -77,21 +108,71 @@ class StandardNode(BaseModel):
77
108
  if field_name == "__typename":
78
109
  response[field_name] = self.get_type()
79
110
  continue
80
- field = getattr(self, field_name)
111
+ field = getattr(self, field_name, None)
81
112
  if field is None:
82
113
  response[field_name] = None
83
114
  continue
115
+ if isinstance(fields.get(field_name), dict):
116
+ result = {}
117
+ for nested_field in fields.get(field_name, {}).keys():
118
+ if nested_field == "value":
119
+ result[nested_field] = field
120
+ continue
121
+ response[field_name] = result
122
+ continue
123
+
84
124
  response[field_name] = field
85
125
 
86
126
  return response
87
127
 
88
- async def save(self, db: InfrahubDatabase) -> bool:
128
+ async def to_graphql(self, fields: StandardNodeQueryFields) -> dict:
129
+ node_response: dict[str, Any] = {}
130
+ meta_response: dict[str, Any] = {}
131
+
132
+ for field_name in fields.node.keys():
133
+ if field_name == "id":
134
+ node_response["id"] = self.uuid
135
+ continue
136
+ if field_name == "__typename":
137
+ node_response[field_name] = self.get_type()
138
+ continue
139
+ field = getattr(self, field_name, None)
140
+ if field is None:
141
+ node_response[field_name] = None
142
+ continue
143
+ if isinstance(fields.node.get(field_name), dict):
144
+ result = {}
145
+ for nested_field in fields.node.get(field_name, {}).keys():
146
+ if nested_field == "value":
147
+ result[nested_field] = field
148
+ continue
149
+ node_response[field_name] = result
150
+ continue
151
+
152
+ node_response[field_name] = field
153
+
154
+ for field_name in fields.node_metadata.keys():
155
+ match field_name:
156
+ case "created_at":
157
+ meta_response["created_at"] = Timestamp(self.created_at).to_datetime() if self.created_at else None
158
+ case "updated_at":
159
+ meta_response["updated_at"] = Timestamp(self.updated_at).to_datetime() if self.updated_at else None
160
+ case "created_by":
161
+ if self.created_by and self.created_by != SYSTEM_USER_ID:
162
+ meta_response["created_by"] = {"id": self.created_by, "__kind__": InfrahubKind.ACCOUNT}
163
+ case "updated_by":
164
+ if self.updated_by and self.updated_by != SYSTEM_USER_ID:
165
+ meta_response["updated_by"] = {"id": self.updated_by, "__kind__": InfrahubKind.ACCOUNT}
166
+
167
+ return {"node": node_response, "node_metadata": meta_response}
168
+
169
+ async def save(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> bool:
89
170
  """Create or Update the Node in the database."""
90
171
 
91
172
  if self.id:
92
- return await self.update(db=db)
173
+ return await self.update(db=db, user_id=user_id)
93
174
 
94
- return await self.create(db=db)
175
+ return await self.create(db=db, user_id=user_id)
95
176
 
96
177
  async def delete(self, db: InfrahubDatabase) -> None:
97
178
  """Delete the Node in the database."""
@@ -99,9 +180,11 @@ class StandardNode(BaseModel):
99
180
  query: Query = await StandardNodeDeleteQuery.init(db=db, node=self)
100
181
  await query.execute(db=db)
101
182
 
102
- async def create(self, db: InfrahubDatabase) -> bool:
183
+ async def create(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> bool:
103
184
  """Create a new node in the database."""
104
-
185
+ self.created_by = user_id
186
+ self.updated_by = self.created_by
187
+ self.updated_at = self.created_at
105
188
  query: Query = await self._query.init(db=db, node=self)
106
189
  await query.execute(db=db)
107
190
 
@@ -115,9 +198,10 @@ class StandardNode(BaseModel):
115
198
 
116
199
  return True
117
200
 
118
- async def update(self, db: InfrahubDatabase) -> bool:
201
+ async def update(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> bool:
119
202
  """Update the node in the database if needed."""
120
-
203
+ self.updated_by = user_id
204
+ self.updated_at = current_timestamp()
121
205
  query: Query = await StandardNodeUpdateQuery.init(db=db, node=self)
122
206
  await query.execute(db=db)
123
207
  result = query.get_result()
@@ -187,7 +271,7 @@ class StandardNode(BaseModel):
187
271
  else:
188
272
  data["uuid"] = str(self.uuid)
189
273
 
190
- for attr_name, field in self.model_fields.items():
274
+ for attr_name, field_info in self.__class__.model_fields.items():
191
275
  if attr_name in self._exclude_attrs:
192
276
  continue
193
277
 
@@ -195,7 +279,7 @@ class StandardNode(BaseModel):
195
279
  if isinstance(attr_value, Enum):
196
280
  attr_value = attr_value.value
197
281
 
198
- field_type = self.guess_field_type(field)
282
+ field_type = self.guess_field_type(field_info)
199
283
 
200
284
  if attr_value is None:
201
285
  data[attr_name] = NULL_VALUE
@@ -219,10 +303,12 @@ class StandardNode(BaseModel):
219
303
  limit: int = 1000,
220
304
  ids: list[str] | None = None,
221
305
  name: str | None = None,
306
+ node_ordering: StandardNodeOrdering | None = None,
222
307
  **kwargs: dict[str, Any],
223
308
  ) -> list[Self]:
309
+ node_ordering = node_ordering or StandardNodeOrdering()
224
310
  query: Query = await StandardNodeGetListQuery.init(
225
- db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs
311
+ db=db, node_class=cls, ids=ids, node_name=name, limit=limit, node_ordering=node_ordering, **kwargs
226
312
  )
227
313
  await query.execute(db=db)
228
314
 
infrahub/core/property.py CHANGED
@@ -34,7 +34,6 @@ class ClearValue(Enum):
34
34
  class FlagPropertyMixin:
35
35
  _flag_properties: list[str] = [v.value for v in FlagProperty]
36
36
 
37
- is_visible = True
38
37
  is_protected = False
39
38
 
40
39
  def _init_flag_property_mixin(self, kwargs: dict | None = None) -> None:
@@ -4,6 +4,8 @@ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
4
4
 
5
5
  from typing_extensions import Self
6
6
 
7
+ from infrahub.core.constants import SYSTEM_USER_ID
8
+
7
9
  if TYPE_CHECKING:
8
10
  from neo4j import AsyncResult, AsyncSession, AsyncTransaction, Record
9
11
 
@@ -82,8 +84,10 @@ class CoreNode(Protocol):
82
84
  at: Timestamp | str | None = None,
83
85
  ) -> Self: ...
84
86
  async def new(self, db: InfrahubDatabase, id: str | None = None, **kwargs: Any) -> Self: ...
85
- async def save(self, db: InfrahubDatabase, at: Timestamp | None = None) -> Self: ...
86
- async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None: ...
87
+ async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, user_id: str = SYSTEM_USER_ID) -> Self: ...
88
+ async def delete(
89
+ self, db: InfrahubDatabase, at: Timestamp | None = None, user_id: str = SYSTEM_USER_ID
90
+ ) -> None: ...
87
91
  async def load(
88
92
  self,
89
93
  db: InfrahubDatabase,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  from collections import defaultdict
4
5
  from dataclasses import dataclass, field
5
6
  from enum import Enum
@@ -12,7 +13,7 @@ from neo4j.graph import Relationship as Neo4jRelationship
12
13
  from opentelemetry import trace
13
14
 
14
15
  from infrahub import config
15
- from infrahub.core.constants import PermissionLevel
16
+ from infrahub.core.constants import SYSTEM_USER_ID, PermissionLevel
16
17
  from infrahub.core.timestamp import Timestamp
17
18
  from infrahub.exceptions import QueryError
18
19
 
@@ -335,7 +336,7 @@ class QueryStat:
335
336
  return cls(**data)
336
337
 
337
338
 
338
- class Query:
339
+ class Query(ABC):
339
340
  name: str = "base-query"
340
341
  type: QueryType
341
342
 
@@ -351,6 +352,7 @@ class Query:
351
352
  offset: int | None = None,
352
353
  order_by: list[str] | None = None,
353
354
  branch_agnostic: bool = False,
355
+ user_id: str = SYSTEM_USER_ID,
354
356
  ):
355
357
  if branch:
356
358
  self.branch = branch
@@ -366,6 +368,7 @@ class Query:
366
368
  self.limit = limit
367
369
  self.offset = offset
368
370
  self.order_by = order_by
371
+ self.user_id = user_id
369
372
 
370
373
  # Initialize internal variables
371
374
  self.params: dict = {}
@@ -402,12 +405,8 @@ class Query:
402
405
 
403
406
  return query
404
407
 
408
+ @abstractmethod
405
409
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
406
- # Avoid using this method for new queries and look at migrating older queries. The
407
- # problem here is that we loose so much information with the `**kwargs` we should instead
408
- # populate this information via the constructor and anything done within the existing query_init methods
409
- # could either be handled within __init__ or via dedicated methods within each Query class where appropriate,
410
- # i.e. things might need to happend in a certain order or we just want to separate the logic better.
411
410
  raise NotImplementedError
412
411
 
413
412
  def get_context(self) -> dict[str, str]:
@@ -48,13 +48,14 @@ class AttributeQuery(Query):
48
48
  class AttributeUpdateValueQuery(AttributeQuery):
49
49
  name = "attribute_update_value"
50
50
  type: QueryType = QueryType.WRITE
51
-
52
- raise_error_if_empty: bool = True
51
+ insert_return: bool = False
52
+ raise_error_if_empty: bool = False
53
53
 
54
54
  async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
55
55
  at = self.at or self.attr.at
56
56
 
57
57
  self.params["attr_uuid"] = self.attr.id
58
+ self.params["user_id"] = self.user_id
58
59
  self.params["branch"] = self.branch.name
59
60
  self.params["branch_level"] = self.branch.hierarchy_level
60
61
  self.params["at"] = at.to_string()
@@ -73,29 +74,46 @@ class AttributeUpdateValueQuery(AttributeQuery):
73
74
  labels.append(GraphAttributeIPNetworkNode.get_default_label())
74
75
 
75
76
  query = """
76
- MATCH (a:Attribute { uuid: $attr_uuid })
77
- MERGE (av:%(labels)s { %(props)s } )
78
- WITH av, a
79
- LIMIT 1
80
- CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
77
+ MATCH (a:Attribute { uuid: $attr_uuid })
78
+ MERGE (av:%(labels)s { %(props)s } )
79
+ WITH av, a
80
+ LIMIT 1
81
+ // ----------
82
+ // find the existing HAS_VALUE edge, if it exists, and set the to time and user_id
83
+ // ---------
84
+ OPTIONAL MATCH (a)-[existing_active_r:%(rel_label)s { branch: $branch, status: "active" }]->()
85
+ WHERE existing_active_r.to IS NULL
86
+ SET existing_active_r.to = $at, existing_active_r.to_user_id = $user_id
87
+ WITH av, a
88
+ LIMIT 1
89
+ // ----------
90
+ // create the new HAS_VALUE edge
91
+ // ---------
92
+ CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at, from_user_id: $user_id }]->(av)
93
+ // ----------
94
+ // update the Attribute node with the new timestamp and user id if we are on the default or global branch
95
+ // ---------
96
+ WITH a
97
+ WHERE $branch_level = 1
98
+ LIMIT 1
99
+ SET a.updated_at = $at, a.updated_by = $user_id
81
100
  """ % {"rel_label": self.attr._rel_to_value_label, "labels": ":".join(labels), "props": ", ".join(prop_list)}
82
101
 
83
102
  self.add_to_query(query)
84
- self.return_labels = ["a", "av", "r"]
85
103
 
86
104
 
87
105
  class AttributeUpdateFlagQuery(AttributeQuery):
88
106
  name = "attribute_update_flag"
89
107
  type: QueryType = QueryType.WRITE
90
-
91
- raise_error_if_empty: bool = True
108
+ insert_return: bool = False
109
+ raise_error_if_empty: bool = False
92
110
 
93
111
  def __init__(
94
112
  self,
95
113
  flag_name: str,
96
114
  **kwargs: Any,
97
115
  ) -> None:
98
- SUPPORTED_FLAGS = ["is_visible", "is_protected"]
116
+ SUPPORTED_FLAGS = ["is_protected"]
99
117
 
100
118
  if flag_name not in SUPPORTED_FLAGS:
101
119
  raise ValueError(f"Only {SUPPORTED_FLAGS} are supported for now.")
@@ -108,6 +126,7 @@ class AttributeUpdateFlagQuery(AttributeQuery):
108
126
  at = self.at or self.attr.at
109
127
 
110
128
  self.params["attr_uuid"] = self.attr.id
129
+ self.params["user_id"] = self.user_id
111
130
  self.params["branch"] = self.branch.name
112
131
  self.params["branch_level"] = self.branch.hierarchy_level
113
132
  self.params["at"] = at.to_string()
@@ -115,20 +134,37 @@ class AttributeUpdateFlagQuery(AttributeQuery):
115
134
  self.params["flag_type"] = self.attr.get_kind()
116
135
 
117
136
  query = """
118
- MATCH (a:Attribute { uuid: $attr_uuid })
119
- MERGE (flag:Boolean { value: $flag_value })
120
- CREATE (a)-[r:%s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(flag)
121
- """ % self.flag_name.upper()
122
-
137
+ MATCH (a:Attribute { uuid: $attr_uuid })
138
+ MERGE (flag:Boolean { value: $flag_value })
139
+ WITH flag, a
140
+ LIMIT 1
141
+ // ----------
142
+ // find the existing property edge, if it exists, and set the to time and user_id
143
+ // ---------
144
+ OPTIONAL MATCH (a)-[existing_active_r:%(flag_type)s { branch: $branch, status: "active" }]->()
145
+ WHERE existing_active_r.to IS NULL
146
+ SET existing_active_r.to = $at, existing_active_r.to_user_id = $user_id
147
+ // ----------
148
+ // create the new property edge
149
+ // ---------
150
+ WITH a, flag
151
+ CREATE (a)-[r:%(flag_type)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at, from_user_id: $user_id }]->(flag)
152
+ // ----------
153
+ // update the Attribute node with the new timestamp and user id if we are on the default or global branch
154
+ // ---------
155
+ WITH a
156
+ WHERE $branch_level = 1
157
+ LIMIT 1
158
+ SET a.updated_at = $at, a.updated_by = $user_id
159
+ """ % {"flag_type": self.flag_name.upper()}
123
160
  self.add_to_query(query)
124
- self.return_labels = ["a", "flag", "r"]
125
161
 
126
162
 
127
163
  class AttributeUpdateNodePropertyQuery(AttributeQuery):
128
164
  name = "attribute_update_node_property"
129
165
  type: QueryType = QueryType.WRITE
130
-
131
- raise_error_if_empty: bool = True
166
+ insert_return: bool = False
167
+ raise_error_if_empty: bool = False
132
168
 
133
169
  def __init__(
134
170
  self,
@@ -147,6 +183,7 @@ class AttributeUpdateNodePropertyQuery(AttributeQuery):
147
183
  branch_filter, branch_params = self.branch.get_query_filter_path(at=at)
148
184
  self.params.update(branch_params)
149
185
  self.params["attr_uuid"] = self.attr.id
186
+ self.params["user_id"] = self.user_id
150
187
  self.params["branch"] = self.branch.name
151
188
  self.params["branch_level"] = self.branch.hierarchy_level
152
189
  self.params["at"] = at.to_string()
@@ -176,13 +213,29 @@ class AttributeUpdateNodePropertyQuery(AttributeQuery):
176
213
  self.add_to_query(node_query)
177
214
 
178
215
  attr_query = """
179
- MATCH (a:Attribute { uuid: $attr_uuid })
180
- CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(np)
216
+ MATCH (a:Attribute { uuid: $attr_uuid })
217
+ // ----------
218
+ // find the existing property edge, if it exists, and set the to time and user_id
219
+ // ---------
220
+ OPTIONAL MATCH (a)-[existing_active_r:%(rel_label)s { branch: $branch, status: "active" }]->()
221
+ WHERE existing_active_r.to IS NULL
222
+ SET existing_active_r.to = $at, existing_active_r.to_user_id = $user_id
223
+ // ----------
224
+ // create the new property edge
225
+ // ---------
226
+ WITH a, np
227
+ LIMIT 1
228
+ CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at, from_user_id: $user_id }]->(np)
229
+ // ----------
230
+ // update the Attribute node with the new timestamp and user id if we are on the default or global branch
231
+ // ---------
232
+ WITH a
233
+ WHERE $branch_level = 1
234
+ LIMIT 1
235
+ SET a.updated_at = $at, a.updated_by = $user_id
181
236
  """ % {"rel_label": rel_label}
182
237
  self.add_to_query(attr_query)
183
238
 
184
- self.return_labels = ["a", "np", "r"]
185
-
186
239
 
187
240
  class AttributeClearNodePropertyQuery(AttributeQuery):
188
241
  name = "attribute_clear_node_property"
@@ -192,11 +245,9 @@ class AttributeClearNodePropertyQuery(AttributeQuery):
192
245
  def __init__(
193
246
  self,
194
247
  prop_name: str,
195
- prop_id: str | None = None,
196
248
  **kwargs: Any,
197
249
  ):
198
250
  self.prop_name = prop_name
199
- self.prop_id = prop_id
200
251
 
201
252
  super().__init__(**kwargs)
202
253
 
@@ -206,15 +257,14 @@ class AttributeClearNodePropertyQuery(AttributeQuery):
206
257
  branch_filter, branch_params = self.branch.get_query_filter_path(at=at)
207
258
  self.params.update(branch_params)
208
259
  self.params["attr_uuid"] = self.attr.id
260
+ self.params["user_id"] = self.user_id
209
261
  self.params["branch"] = self.branch.name
210
262
  self.params["branch_level"] = self.branch.hierarchy_level
211
263
  self.params["at"] = at.to_string()
212
- self.params["prop_name"] = self.prop_name
213
- self.params["prop_id"] = self.prop_id
214
264
 
215
265
  rel_label = f"HAS_{self.prop_name.upper()}"
216
266
  query = """
217
- MATCH (a:Attribute { uuid: $attr_uuid })-[r:%(rel_label)s]->(np:Node { uuid: $prop_id })
267
+ MATCH (a:Attribute { uuid: $attr_uuid })-[r:%(rel_label)s]->(np:Node)
218
268
  WITH DISTINCT a, np
219
269
  CALL (a, np) {
220
270
  MATCH (a)-[r:%(rel_label)s]->(np)
@@ -228,42 +278,107 @@ WHERE property_edge.status = "active"
228
278
  CALL (property_edge) {
229
279
  WITH property_edge
230
280
  WHERE property_edge.branch = $branch
231
- SET property_edge.to = $at
281
+ SET property_edge.to = $at, property_edge.to_user_id = $user_id
232
282
  }
233
283
  CALL (a, np, property_edge) {
234
284
  WITH property_edge
235
285
  WHERE property_edge.branch_level < $branch_level
236
- CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at }]->(np)
286
+ CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]->(np)
287
+ }
288
+ CALL (a) {
289
+ WITH a
290
+ WHERE $branch_level = 1
291
+ LIMIT 1
292
+ SET a.updated_at = $at, a.updated_by = $user_id
237
293
  }
238
294
  """ % {"branch_filter": branch_filter, "rel_label": rel_label}
239
295
  self.add_to_query(query)
240
296
 
241
297
 
242
- class AttributeGetQuery(AttributeQuery):
243
- name = "attribute_get"
244
- type: QueryType = QueryType.READ
298
+ class AttributeDeleteQuery(AttributeQuery):
299
+ name = "attribute_delete"
300
+ type: QueryType = QueryType.WRITE
301
+ insert_return: bool = False
245
302
 
246
303
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
247
304
  self.params["attr_uuid"] = self.attr.id
248
- self.params["node_uuid"] = self.attr.node.id
249
-
305
+ self.params["user_id"] = self.user_id
306
+ self.params["branch"] = self.branch.name
307
+ self.params["branch_level"] = self.branch.hierarchy_level
308
+ self.params["branched_from"] = self.branch.get_branched_from()
250
309
  self.params["at"] = self.at.to_string()
251
310
 
252
- rels_filter, rels_params = self.branch.get_query_filter_path(at=self.at.to_string())
253
- self.params.update(rels_params)
311
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
312
+ self.params.update(branch_params)
254
313
 
255
- query = (
256
- """
257
- MATCH (a:Attribute { uuid: $attr_uuid })
258
- MATCH p = ((a)-[r2:HAS_VALUE|IS_VISIBLE|IS_PROTECTED|HAS_SOURCE|HAS_OWNER]->(ap))
259
- WHERE all(r IN relationships(p) WHERE ( %s ))
260
- """
261
- % rels_filter
262
- )
314
+ query = """
315
+ MATCH (a:Attribute { uuid: $attr_uuid })
316
+ CALL (a) {
317
+ WITH a
318
+ WHERE $branch_level = 1
319
+ LIMIT 1
320
+ SET a.updated_at = $at, a.updated_by = $user_id
321
+ }
263
322
 
323
+ UNWIND [
324
+ ["HAS_ATTRIBUTE", "in"],
325
+ ["HAS_VALUE", "out"],
326
+ ["IS_PROTECTED", "out"],
327
+ ["HAS_SOURCE", "out"],
328
+ ["HAS_OWNER", "out"]
329
+ ] AS edge_details
330
+ WITH a, edge_details[0] AS property_type, edge_details[1] AS direction
331
+ CALL (a, property_type, direction) {
332
+ MATCH (a)-[r]-(attr_peer)
333
+ WHERE type(r) = property_type
334
+ AND (
335
+ (direction = "in" AND startNode(r) = attr_peer)
336
+ OR (direction = "out" AND startNode(r) = a)
337
+ )
338
+ AND %(branch_filter)s
339
+ RETURN r AS property_edge, attr_peer
340
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
341
+ LIMIT 1
342
+ }
343
+ CALL (property_edge) {
344
+ WITH property_edge
345
+ WHERE property_edge.status = "active"
346
+ AND property_edge.branch = $branch
347
+ AND property_edge.to IS NULL
348
+ SET property_edge.to = $at, property_edge.to_user_id = $user_id
349
+ }
350
+ WITH a, property_edge, property_type, attr_peer, direction
351
+ CALL (a, property_type, attr_peer, direction) {
352
+ WITH direction
353
+ WHERE direction = "out"
354
+ CREATE (a)
355
+ -[r:$(property_type) { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]
356
+ ->(attr_peer)
357
+ }
358
+ CALL (a, property_type, attr_peer, direction) {
359
+ WITH direction
360
+ WHERE direction = "in"
361
+ CREATE (a)
362
+ <-[r:$(property_type) { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]
363
+ -(attr_peer)
364
+ }
365
+ WITH CASE
366
+ WHEN property_type = "HAS_VALUE" THEN attr_peer.value
367
+ ELSE NULL
368
+ END AS property_value
369
+ WITH property_value
370
+ RETURN property_value
371
+ ORDER BY property_value ASC
372
+ LIMIT 1
373
+ """ % {"branch_filter": branch_filter}
264
374
  self.add_to_query(query)
375
+ self.return_labels = ["property_value"]
265
376
 
266
- self.return_labels = ["a", "ap", "r2"]
377
+ def get_previous_property_value(self) -> Any:
378
+ result = self.get_result()
379
+ if result:
380
+ return result.get(label="property_value")
381
+ return None
267
382
 
268
383
 
269
384
  async def default_attribute_query_filter(