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
infrahub/actions/tasks.py CHANGED
@@ -211,8 +211,10 @@ async def configure_action_rules(
211
211
  service: InfrahubServices,
212
212
  ) -> None:
213
213
  await setup_triggers_specific(
214
- gatherer=gather_trigger_action_rules, trigger_type=TriggerType.ACTION_TRIGGER_RULE, db=service.database
215
- ) # type: ignore[misc]
214
+ gatherer=gather_trigger_action_rules, # type: ignore[arg-type]
215
+ trigger_type=TriggerType.ACTION_TRIGGER_RULE,
216
+ db=service.database,
217
+ )
216
218
 
217
219
 
218
220
  async def _get_targets(
infrahub/api/schema.py CHANGED
@@ -366,6 +366,7 @@ async def load_schema(
366
366
  diff=result.diff,
367
367
  limit=result.diff.all,
368
368
  update_db=True,
369
+ user_id=account_session.account_id,
369
370
  )
370
371
  branch.update_schema_hash()
371
372
  log.info("Schema has been updated", branch=branch.name, hash=branch.active_schema_hash.main)
@@ -375,7 +376,7 @@ async def load_schema(
375
376
  branch.is_isolated = True
376
377
  log.info("Branch converted to isolated mode because the schema has changed", branch=branch.name)
377
378
 
378
- await branch.save(db=dbt)
379
+ await branch.save(db=dbt, user_id=account_session.account_id)
379
380
  updated_branch = registry.schema.get_schema_branch(name=branch.name)
380
381
  updated_hash = updated_branch.get_hash()
381
382
 
@@ -387,6 +388,7 @@ async def load_schema(
387
388
  new_schema=candidate_schema,
388
389
  previous_schema=origin_schema,
389
390
  migrations=result.migrations,
391
+ user_id=account_session.account_id,
390
392
  )
391
393
  migration_error_msgs = await service.workflow.execute_workflow(
392
394
  workflow=SCHEMA_APPLY_MIGRATION,
@@ -14,6 +14,7 @@ async def create(model: CheckArtifactCreate) -> ValidatorConclusion:
14
14
  await add_tags(branches=[model.branch_name], nodes=[model.target_id])
15
15
 
16
16
  client = get_client()
17
+ client.request_context = model.context.to_request_context()
17
18
 
18
19
  validator = await client.get(kind=InfrahubKind.ARTIFACTVALIDATOR, id=model.validator_id, include=["checks"])
19
20
 
infrahub/auth.py CHANGED
@@ -73,8 +73,8 @@ async def authenticate_with_password(
73
73
  ) -> models.UserToken:
74
74
  selected_branch = await registry.get_branch(db=db, branch=branch)
75
75
 
76
- response: list[CoreGenericAccount] = await NodeManager.query(
77
- schema=InfrahubKind.GENERICACCOUNT,
76
+ response = await NodeManager.query(
77
+ schema=CoreGenericAccount,
78
78
  db=db,
79
79
  branch=selected_branch,
80
80
  filters={"name__value": credentials.username},
infrahub/cli/db.py CHANGED
@@ -603,17 +603,17 @@ RETURN vertices, edges
603
603
  edge_path = export_dir / Path("edges.csv")
604
604
  edge_path.touch(exist_ok=True)
605
605
 
606
- graph_node_schemas = [GraphNodeProperties, GraphRelationshipProperties, GraphAttributeProperties]
606
+ graph_node_schemas = (GraphNodeProperties, GraphRelationshipProperties, GraphAttributeProperties)
607
607
  graph_vertex_properties = set()
608
- for graph_schema in graph_node_schemas:
609
- for field_name, field_info in graph_schema.model_fields.items():
608
+ for graph_node_schema in graph_node_schemas:
609
+ for field_name, field_info in graph_node_schema.model_fields.items():
610
610
  property_name = field_info.alias or field_name
611
611
  graph_vertex_properties.add(property_name)
612
612
 
613
- graph_edge_schemas = [GraphRelationshipIsPartOf, GraphRelationshipDefault]
613
+ graph_edge_schemas = (GraphRelationshipIsPartOf, GraphRelationshipDefault)
614
614
  graph_edge_properties = set()
615
- for graph_schema in graph_edge_schemas:
616
- for field_name, field_info in graph_schema.model_fields.items():
615
+ for graph_edge_schema in graph_edge_schemas:
616
+ for field_name, field_info in graph_edge_schema.model_fields.items():
617
617
  property_name = field_info.alias or field_name
618
618
  graph_edge_properties.add(property_name)
619
619
 
@@ -7,9 +7,9 @@ from prefect import task
7
7
  from prefect.cache_policies import NONE
8
8
  from prefect.logging import get_run_logger
9
9
 
10
- from infrahub.core.constants import InfrahubKind
11
10
  from infrahub.core.manager import NodeManager
12
11
  from infrahub.core.protocols import CoreGenericRepository, CoreGraphQLQuery
12
+ from infrahub.core.protocols import CoreTransformPython as CoreTransformPythonNode
13
13
  from infrahub.core.registry import registry
14
14
  from infrahub.database import InfrahubDatabase # noqa: TC001 needed for prefect flow
15
15
  from infrahub.git.utils import get_repositories_commit_per_branch
@@ -24,7 +24,6 @@ from .models import (
24
24
  )
25
25
 
26
26
  if TYPE_CHECKING:
27
- from infrahub.core.protocols import CoreTransformPython as CoreTransformPythonNode
28
27
  from infrahub.git.models import RepositoryData
29
28
 
30
29
 
@@ -48,9 +47,9 @@ async def gather_python_transform_attributes(
48
47
  if not transform_names:
49
48
  return []
50
49
 
51
- transforms: list[CoreTransformPythonNode] = await NodeManager.query(
50
+ transforms = await NodeManager.query(
52
51
  db=db,
53
- schema=InfrahubKind.TRANSFORMPYTHON,
52
+ schema=CoreTransformPythonNode,
54
53
  branch=branch_name,
55
54
  fields={"id": None, "name": None, "repository": None, "query": None},
56
55
  filters={"name__values": transform_names},
@@ -42,8 +42,10 @@ mutation UpdateAttribute(
42
42
  $kind: String!,
43
43
  $attribute: String!,
44
44
  $value: String!
45
+ $context_account_id: String!
45
46
  ) {
46
47
  InfrahubUpdateComputedAttribute(
48
+ context: {account: {id: $context_account_id}},
47
49
  data: {id: $id, attribute: $attribute, value: $value, kind: $kind}
48
50
  ) {
49
51
  ok
@@ -62,7 +64,7 @@ async def process_transform(
62
64
  object_id: str,
63
65
  computed_attribute_name: str, # noqa: ARG001
64
66
  computed_attribute_kind: str, # noqa: ARG001
65
- context: InfrahubContext, # noqa: ARG001
67
+ context: InfrahubContext,
66
68
  updated_fields: list[str] | None = None, # noqa: ARG001
67
69
  ) -> None:
68
70
  await add_tags(branches=[branch_name], nodes=[object_id])
@@ -103,7 +105,7 @@ async def process_transform(
103
105
  name=transform.repository.peer.name.value,
104
106
  repository_kind=str(transform.repository.peer.typename),
105
107
  commit=repo_node.commit.value,
106
- ) # type: ignore[misc]
108
+ )
107
109
 
108
110
  data = await client.query_gql_query(
109
111
  name=transform.query.id,
@@ -120,11 +122,17 @@ async def process_transform(
120
122
  location=f"{transform.file_path.value}::{transform.class_name.value}",
121
123
  data=data,
122
124
  convert_query_response=transform.convert_query_response.value,
123
- ) # type: ignore[misc]
125
+ ) # type: ignore[call-overload]
124
126
 
125
127
  await client.execute_graphql(
126
128
  query=UPDATE_ATTRIBUTE,
127
- variables={"id": object_id, "kind": node_kind, "attribute": attribute_name, "value": transformed_data},
129
+ variables={
130
+ "id": object_id,
131
+ "kind": node_kind,
132
+ "attribute": attribute_name,
133
+ "value": transformed_data,
134
+ "context_account_id": context.account.account_id,
135
+ },
128
136
  branch_name=branch_name,
129
137
  )
130
138
 
@@ -168,6 +176,7 @@ async def computed_attribute_jinja2_update_value(
168
176
  node_kind: str,
169
177
  attribute_name: str,
170
178
  template: Jinja2Template,
179
+ context: InfrahubContext,
171
180
  ) -> None:
172
181
  log = get_run_logger()
173
182
  client = get_client()
@@ -182,7 +191,13 @@ async def computed_attribute_jinja2_update_value(
182
191
  try:
183
192
  await client.execute_graphql(
184
193
  query=UPDATE_ATTRIBUTE,
185
- variables={"id": obj.node_id, "kind": node_kind, "attribute": attribute_name, "value": value},
194
+ variables={
195
+ "id": obj.node_id,
196
+ "kind": node_kind,
197
+ "attribute": attribute_name,
198
+ "value": value,
199
+ "context_account_id": context.account.account_id,
200
+ },
186
201
  branch_name=branch_name,
187
202
  )
188
203
  log.info(f"Updating computed attribute {node_kind}.{attribute_name}='{value}' ({obj.node_id})")
@@ -202,7 +217,7 @@ async def process_jinja2(
202
217
  object_id: str,
203
218
  computed_attribute_name: str,
204
219
  computed_attribute_kind: str,
205
- context: InfrahubContext, # noqa: ARG001
220
+ context: InfrahubContext,
206
221
  updated_fields: list[str] | None = None,
207
222
  ) -> None:
208
223
  log = get_run_logger()
@@ -258,6 +273,7 @@ async def process_jinja2(
258
273
  node_kind=node_schema.kind,
259
274
  attribute_name=computed_macro.attribute.name,
260
275
  template=jinja_template,
276
+ context=context,
261
277
  )
262
278
 
263
279
  _ = [response async for _, response in batch.execute()]
@@ -462,6 +478,7 @@ async def query_transform_targets(
462
478
  "object_id": subscriber.object_id,
463
479
  "computed_attribute_name": computed_attribute.name,
464
480
  "computed_attribute_kind": subscriber.kind,
481
+ "context": context,
465
482
  },
466
483
  )
467
484
 
infrahub/config.py CHANGED
@@ -207,6 +207,14 @@ class MainSettings(BaseSettings):
207
207
  def convert_to_path(cls, value: Path | str) -> Path:
208
208
  return Path(value) if isinstance(value, str) else value
209
209
 
210
+ @property
211
+ def infrahub_address(self) -> str:
212
+ """This is the address that the Prefect worker will use to connect to Infrahub API."""
213
+ if self.internal_address:
214
+ return self.internal_address
215
+
216
+ raise InitializationError()
217
+
210
218
 
211
219
  class FileSystemStorageSettings(BaseSettings):
212
220
  # Make variable lookup case-sensitive to avoid fetching $PATH value
@@ -0,0 +1,12 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class OrderDirection(StrEnum):
5
+ ASC = "ASC"
6
+ DESC = "DESC"
7
+
8
+
9
+ class OrderByField(StrEnum):
10
+ ID = "id"
11
+ CREATED_AT = "created_at"
12
+ UPDATED_AT = "updated_at"
infrahub/core/account.py CHANGED
@@ -54,15 +54,11 @@ class AccountGlobalPermissionQuery(Query):
54
54
  name: str = "account_global_permissions"
55
55
  type: QueryType = QueryType.READ
56
56
 
57
- def __init__(
58
- self,
59
- account_id: str,
60
- branch: Branch | None = None,
61
- branch_agnostic: bool = False,
62
- ) -> None:
63
- super().__init__(branch=branch, branch_agnostic=branch_agnostic)
57
+ def __init__(self, account_id: str, **kwargs: Any):
64
58
  self.account_id = account_id
59
+ super().__init__(**kwargs)
65
60
 
61
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
66
62
  self.params["account_id"] = self.account_id
67
63
 
68
64
  branch_filter, branch_params = self.branch.get_query_filter_path(
@@ -70,6 +66,7 @@ class AccountGlobalPermissionQuery(Query):
70
66
  )
71
67
  self.params.update(branch_params)
72
68
 
69
+ # ruff: noqa: E501
73
70
  query = """
74
71
  MATCH (account:%(generic_account_node)s)
75
72
  WHERE account.uuid = $account_id
@@ -296,7 +293,7 @@ class AccountObjectPermissionQuery(Query):
296
293
 
297
294
 
298
295
  async def fetch_permissions(account_id: str, db: InfrahubDatabase, branch: Branch) -> AssignedPermissions:
299
- query1 = AccountGlobalPermissionQuery(branch=branch, account_id=account_id, branch_agnostic=True)
296
+ query1 = await AccountGlobalPermissionQuery.init(db=db, branch=branch, account_id=account_id, branch_agnostic=True)
300
297
  await query1.execute(db=db)
301
298
  global_permissions = query1.get_permissions()
302
299
 
@@ -15,18 +15,27 @@ from pydantic import BaseModel, Field
15
15
  from infrahub import config
16
16
  from infrahub.core import registry
17
17
  from infrahub.core.changelog.models import AttributeChangelog
18
- from infrahub.core.constants import NULL_VALUE, AttributeDBNodeType, BranchSupportType, RelationshipStatus
18
+ from infrahub.core.constants import (
19
+ NULL_VALUE,
20
+ SYSTEM_USER_ID,
21
+ AttributeDBNodeType,
22
+ BranchSupportType,
23
+ InfrahubKind,
24
+ MetadataOptions,
25
+ )
26
+ from infrahub.core.metadata.interface import MetadataInterface
27
+ from infrahub.core.metadata.model import MetadataInfo
19
28
  from infrahub.core.property import FlagPropertyMixin, NodePropertyData, NodePropertyMixin
20
29
  from infrahub.core.query.attribute import (
21
30
  AttributeClearNodePropertyQuery,
22
- AttributeGetQuery,
31
+ AttributeDeleteQuery,
23
32
  AttributeUpdateFlagQuery,
24
33
  AttributeUpdateNodePropertyQuery,
25
34
  AttributeUpdateValueQuery,
26
35
  )
27
36
  from infrahub.core.query.node import AttributeFromDB, NodeListGetAttributeQuery
28
37
  from infrahub.core.timestamp import Timestamp
29
- from infrahub.core.utils import add_relationship, convert_ip_to_binary_str, update_relationships_to
38
+ from infrahub.core.utils import convert_ip_to_binary_str
30
39
  from infrahub.exceptions import ValidationError
31
40
  from infrahub.helpers import hash_password
32
41
 
@@ -70,12 +79,11 @@ class AttributeCreateData(BaseModel):
70
79
  content: dict[str, Any]
71
80
  is_default: bool
72
81
  is_protected: bool
73
- is_visible: bool
74
82
  source_prop: list[NodePropertyData] = Field(default_factory=list)
75
83
  owner_prop: list[NodePropertyData] = Field(default_factory=list)
76
84
 
77
85
 
78
- class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
86
+ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin, MetadataInterface):
79
87
  type: type | tuple[type] | None = None
80
88
 
81
89
  _rel_to_node_label: str = RELATIONSHIP_TO_NODE_LABEL
@@ -91,7 +99,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
91
99
  id: str | None = None,
92
100
  db_id: str | None = None,
93
101
  data: dict | str | AttributeFromDB | None = None,
94
- updated_at: Timestamp | str | None = None,
95
102
  is_default: bool = False,
96
103
  is_from_profile: bool = False,
97
104
  **kwargs: dict[str, Any],
@@ -99,7 +106,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
99
106
  self.id = id
100
107
  self.db_id = db_id
101
108
 
102
- self.updated_at = updated_at
103
109
  self.name = name
104
110
  self.node = node
105
111
  self.schema = schema
@@ -112,6 +118,13 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
112
118
  self._init_node_property_mixin(kwargs)
113
119
  self._init_flag_property_mixin(kwargs)
114
120
 
121
+ self._metadata = MetadataInfo(
122
+ created_at=kwargs.get("created_at"),
123
+ created_by=kwargs.get("created_by"),
124
+ updated_at=kwargs.get("updated_at"),
125
+ updated_by=kwargs.get("updated_by"),
126
+ )
127
+
115
128
  self.value = None
116
129
 
117
130
  if isinstance(data, AttributeFromDB):
@@ -130,8 +143,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
130
143
  for field_name in fields_to_extract_from_data:
131
144
  setattr(self, field_name, data.get(field_name, None))
132
145
 
133
- if not self.updated_at and "updated_at" in data:
134
- self.updated_at = Timestamp(data.get("updated_at"))
135
146
  elif data is None:
136
147
  self.is_default = True
137
148
  else:
@@ -151,9 +162,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
151
162
  if self.is_protected is None:
152
163
  self.is_protected = False
153
164
 
154
- if self.is_visible is None:
155
- self.is_visible = True
156
-
157
165
  @property
158
166
  def is_enum(self) -> bool:
159
167
  return bool(self.schema.enum)
@@ -187,6 +195,30 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
187
195
  if self.is_enum and self.value:
188
196
  self.value = self.schema.convert_value_to_enum(self.value)
189
197
 
198
+ def _set_created_at(self, value: Timestamp | None) -> None:
199
+ self._metadata.created_at = value
200
+
201
+ def _set_created_by(self, value: str | None) -> None:
202
+ self._metadata.created_by = value
203
+
204
+ def _set_updated_at(self, value: Timestamp | None) -> None:
205
+ self._metadata.updated_at = value
206
+
207
+ def _set_updated_by(self, value: str | None) -> None:
208
+ self._metadata.updated_by = value
209
+
210
+ def _get_created_at(self) -> Timestamp | None:
211
+ return self._metadata.created_at
212
+
213
+ def _get_created_by(self) -> str | None:
214
+ return self._metadata.created_by
215
+
216
+ def _get_updated_at(self) -> Timestamp | None:
217
+ return self._metadata.updated_at
218
+
219
+ def _get_updated_by(self) -> str | None:
220
+ return self._metadata.updated_by
221
+
190
222
  @staticmethod
191
223
  def get_allowed_property_in_path() -> list[str]:
192
224
  return ["value"]
@@ -302,8 +334,10 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
302
334
  if prop_name in data.node_properties:
303
335
  setattr(self, prop_name, data.node_properties[prop_name].uuid)
304
336
 
305
- if not self.updated_at and data.updated_at:
306
- self.updated_at = Timestamp(data.updated_at)
337
+ self._set_created_at(data.created_at)
338
+ self._set_created_by(data.created_by)
339
+ self._set_updated_at(data.updated_at)
340
+ self._set_updated_by(data.updated_by)
307
341
 
308
342
  def value_from_db(self, data: AttributeFromDB) -> Any:
309
343
  if data.value == NULL_VALUE:
@@ -320,7 +354,9 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
320
354
  """Deserialize the value coming from the database."""
321
355
  return data.value
322
356
 
323
- async def save(self, db: InfrahubDatabase, at: Timestamp | None = None) -> AttributeChangelog | None:
357
+ async def save(
358
+ self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None
359
+ ) -> AttributeChangelog | None:
324
360
  """Create or Update the Attribute in the database."""
325
361
 
326
362
  save_at = Timestamp(at)
@@ -328,70 +364,34 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
328
364
  if not self.id:
329
365
  return None
330
366
 
331
- return await self._update(at=save_at, db=db)
367
+ return await self._update(db=db, user_id=user_id, at=save_at)
332
368
 
333
- async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> AttributeChangelog | None:
369
+ async def delete(
370
+ self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None
371
+ ) -> AttributeChangelog | None:
334
372
  if not self.db_id:
335
373
  return None
336
374
 
337
375
  delete_at = Timestamp(at)
376
+ branch = self.get_branch_based_on_support_type()
338
377
 
339
- query = await AttributeGetQuery.init(db=db, attr=self, at=delete_at)
378
+ query = await AttributeDeleteQuery.init(db=db, branch=branch, attr=self, user_id=user_id, at=delete_at)
340
379
  await query.execute(db=db)
341
- results = query.get_results()
380
+ previous_value = query.get_previous_property_value()
342
381
 
343
- if not results:
382
+ if not previous_value:
344
383
  return None
345
384
 
346
- changelog = AttributeChangelog(
385
+ return AttributeChangelog(
347
386
  name=self.name,
348
387
  value=None,
349
- value_previous=None,
388
+ value_previous=previous_value,
350
389
  kind=self.schema.kind,
351
390
  )
352
391
 
353
- properties_to_delete = []
354
- branch = self.get_branch_based_on_support_type()
355
-
356
- # Check all the relationship and update the one that are in the same branch
357
- rel_ids_to_update = set()
358
- for result in results:
359
- if result.get_rel("r2").type == "HAS_VALUE":
360
- changelog.value_previous = result.get_node("ap").get("value")
361
- properties_to_delete.append((result.get_rel("r2").type, result.get_node("ap").element_id))
362
-
363
- await add_relationship(
364
- src_node_id=self.db_id,
365
- dst_node_id=result.get_node("ap").element_id,
366
- rel_type=result.get_rel("r2").type,
367
- branch_name=branch.name,
368
- branch_level=branch.hierarchy_level,
369
- at=delete_at,
370
- status=RelationshipStatus.DELETED,
371
- db=db,
372
- )
373
-
374
- for rel in result.get_rels():
375
- if rel.get("branch") == branch.name:
376
- rel_ids_to_update.add(rel.element_id)
377
-
378
- if rel_ids_to_update:
379
- await update_relationships_to(ids=list(rel_ids_to_update), to=delete_at, db=db)
380
-
381
- await add_relationship(
382
- src_node_id=self.node.db_id,
383
- dst_node_id=self.db_id,
384
- rel_type="HAS_ATTRIBUTE",
385
- branch_name=branch.name,
386
- branch_level=branch.hierarchy_level,
387
- at=delete_at,
388
- status=RelationshipStatus.DELETED,
389
- db=db,
390
- )
391
-
392
- return changelog
393
-
394
- async def _update(self, db: InfrahubDatabase, at: Timestamp | None = None) -> AttributeChangelog | None:
392
+ async def _update(
393
+ self, db: InfrahubDatabase, user_id: str, at: Timestamp | None = None
394
+ ) -> AttributeChangelog | None:
395
395
  """Update the attribute in the database.
396
396
 
397
397
  Get the current value
@@ -422,13 +422,10 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
422
422
  fields={self.name: True},
423
423
  branch=self.branch,
424
424
  at=update_at,
425
- include_source=True,
426
- include_owner=True,
425
+ include_metadata=MetadataOptions.LINKED_NODES,
427
426
  )
428
427
  await query.execute(db=db)
429
- current_attr_data, current_attr_result = query.get_result_by_id_and_name(self.node.id, self.name)
430
-
431
- branch = self.get_branch_based_on_support_type()
428
+ current_attr_data, _ = query.get_result_by_id_and_name(self.node.id, self.name)
432
429
 
433
430
  changelog = AttributeChangelog(
434
431
  name=self.name,
@@ -437,36 +434,26 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
437
434
  kind=self.schema.kind,
438
435
  )
439
436
 
437
+ branch = self.get_branch_based_on_support_type()
438
+
440
439
  # ---------- Update the Value ----------
441
440
  if current_attr_data.content != self.to_db():
442
441
  # Create the new AttributeValue and update the existing relationship
443
- query = await AttributeUpdateValueQuery.init(db=db, attr=self, at=update_at)
442
+ query = await AttributeUpdateValueQuery.init(db=db, branch=branch, attr=self, user_id=user_id, at=update_at)
444
443
  await query.execute(db=db)
445
444
 
446
- # TODO check that everything went well
447
- rel = current_attr_result.get_rel("r2")
448
- if rel.get("branch") == branch.name:
449
- await update_relationships_to([rel.element_id], to=update_at, db=db)
450
-
451
445
  # ---------- Update the Flags ----------
452
- SUPPORTED_FLAGS = (
453
- ("is_visible", "isv", "rel_isv"),
454
- ("is_protected", "isp", "rel_isp"),
455
- )
456
-
457
- for flag_name, _, rel_name in SUPPORTED_FLAGS:
458
- if current_attr_data.flag_properties[flag_name] != getattr(self, flag_name):
459
- changelog.add_property(
460
- name=flag_name,
461
- value_current=getattr(self, flag_name),
462
- value_previous=current_attr_data.flag_properties[flag_name],
463
- )
464
- query = await AttributeUpdateFlagQuery.init(db=db, attr=self, at=update_at, flag_name=flag_name)
465
- await query.execute(db=db)
466
-
467
- rel = current_attr_result.get(rel_name)
468
- if rel.get("branch") == branch.name:
469
- await update_relationships_to([rel.element_id], to=update_at, db=db)
446
+ flag_name = "is_protected"
447
+ if current_attr_data.flag_properties[flag_name] != getattr(self, flag_name):
448
+ changelog.add_property(
449
+ name=flag_name,
450
+ value_current=getattr(self, flag_name),
451
+ value_previous=current_attr_data.flag_properties[flag_name],
452
+ )
453
+ query = await AttributeUpdateFlagQuery.init(
454
+ db=db, branch=branch, attr=self, user_id=user_id, at=update_at, flag_name=flag_name
455
+ )
456
+ await query.execute(db=db)
470
457
 
471
458
  # ---------- Update the Node Properties ----------
472
459
  for prop_name in self._node_properties:
@@ -488,21 +475,27 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
488
475
 
489
476
  if needs_update:
490
477
  query = await AttributeUpdateNodePropertyQuery.init(
491
- db=db, attr=self, at=update_at, prop_name=prop_name, prop_id=current_prop_id
478
+ db=db,
479
+ branch=branch,
480
+ attr=self,
481
+ user_id=user_id,
482
+ at=update_at,
483
+ prop_name=prop_name,
484
+ prop_id=current_prop_id,
492
485
  )
493
486
  await query.execute(db=db)
494
487
 
495
488
  if needs_clear:
496
489
  query = await AttributeClearNodePropertyQuery.init(
497
- db=db, attr=self, at=update_at, prop_name=prop_name, prop_id=database_prop_id
490
+ db=db,
491
+ branch=branch,
492
+ attr=self,
493
+ user_id=user_id,
494
+ at=update_at,
495
+ prop_name=prop_name,
498
496
  )
499
497
  await query.execute(db=db)
500
498
 
501
- # set the to time on the previously active edge
502
- rel = current_attr_result.get(f"rel_{prop_name}")
503
- if rel and rel.get("branch") == branch.name:
504
- await update_relationships_to([rel.element_id], to=update_at, db=db)
505
-
506
499
  if changelog.has_updates:
507
500
  return changelog
508
501
 
@@ -531,10 +524,19 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
531
524
 
532
525
  for field_name in field_names:
533
526
  if field_name == "updated_at":
534
- if self.updated_at:
535
- response[field_name] = await self.updated_at.to_graphql()
536
- else:
537
- response[field_name] = None
527
+ updated_at = self._get_updated_at()
528
+ response[field_name] = await updated_at.to_graphql() if updated_at else None
529
+ continue
530
+
531
+ if field_name == "updated_by":
532
+ response[field_name] = (
533
+ {
534
+ "id": self._get_updated_by(),
535
+ "__kind__": InfrahubKind.ACCOUNT,
536
+ }
537
+ if self._get_updated_by()
538
+ else None
539
+ )
538
540
  continue
539
541
 
540
542
  if field_name == "__typename":
@@ -620,9 +622,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
620
622
  if "is_protected" in data and data["is_protected"] != self.is_protected:
621
623
  self.is_protected = data["is_protected"]
622
624
  changed = True
623
- if "is_visible" in data and data["is_visible"] != self.is_visible:
624
- self.is_visible = data["is_visible"]
625
- changed = True
626
625
 
627
626
  if "source" in data and data["source"] != self.source_id:
628
627
  self.source = data["source"]
@@ -657,7 +656,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
657
656
  content=self.to_db(),
658
657
  is_default=self.is_default,
659
658
  is_protected=self.is_protected,
660
- is_visible=self.is_visible,
661
659
  )
662
660
  if self.source_id:
663
661
  data.source_prop.append(NodePropertyData(name="source", peer_id=self.source_id))