infrahub-server 1.6.3__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +4 -2
- infrahub/api/exceptions.py +2 -2
- infrahub/api/schema.py +3 -1
- infrahub/artifacts/tasks.py +1 -0
- infrahub/auth.py +2 -2
- infrahub/cli/db.py +54 -28
- infrahub/computed_attribute/gather.py +3 -4
- infrahub/computed_attribute/tasks.py +23 -6
- infrahub/config.py +8 -0
- infrahub/constants/enums.py +12 -0
- infrahub/core/account.py +4 -4
- infrahub/core/attribute.py +106 -108
- infrahub/core/branch/models.py +44 -71
- infrahub/core/branch/tasks.py +5 -3
- infrahub/core/changelog/diff.py +1 -20
- infrahub/core/changelog/models.py +0 -7
- infrahub/core/constants/__init__.py +17 -0
- infrahub/core/constants/database.py +0 -1
- infrahub/core/constants/schema.py +0 -1
- infrahub/core/convert_object_type/repository_conversion.py +3 -4
- infrahub/core/diff/branch_differ.py +1 -1
- infrahub/core/diff/conflict_transferer.py +1 -1
- infrahub/core/diff/data_check_synchronizer.py +4 -3
- infrahub/core/diff/enricher/cardinality_one.py +2 -2
- infrahub/core/diff/enricher/hierarchy.py +1 -1
- infrahub/core/diff/enricher/labels.py +1 -1
- infrahub/core/diff/merger/merger.py +28 -2
- infrahub/core/diff/merger/serializer.py +3 -10
- infrahub/core/diff/model/diff.py +1 -1
- infrahub/core/diff/query/merge.py +376 -135
- infrahub/core/diff/repository/repository.py +3 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +3 -3
- infrahub/core/graph/schema.py +2 -12
- infrahub/core/ipam/reconciler.py +8 -6
- infrahub/core/ipam/utilization.py +8 -15
- infrahub/core/manager.py +133 -152
- infrahub/core/merge.py +1 -1
- infrahub/core/metadata/__init__.py +0 -0
- infrahub/core/metadata/interface.py +37 -0
- infrahub/core/metadata/model.py +31 -0
- infrahub/core/metadata/query/__init__.py +0 -0
- infrahub/core/metadata/query/node_metadata.py +301 -0
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
- infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
- infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
- infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
- infrahub/core/migrations/query/__init__.py +2 -2
- infrahub/core/migrations/query/attribute_add.py +17 -6
- infrahub/core/migrations/query/attribute_remove.py +19 -5
- infrahub/core/migrations/query/attribute_rename.py +21 -5
- infrahub/core/migrations/query/node_duplicate.py +19 -4
- infrahub/core/migrations/query/schema_attribute_update.py +1 -1
- infrahub/core/migrations/schema/attribute_kind_update.py +21 -2
- infrahub/core/migrations/schema/attribute_name_update.py +1 -1
- infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
- infrahub/core/migrations/schema/models.py +3 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
- infrahub/core/migrations/schema/node_kind_update.py +1 -1
- infrahub/core/migrations/schema/node_remove.py +24 -2
- infrahub/core/migrations/schema/tasks.py +4 -1
- infrahub/core/migrations/shared.py +13 -6
- infrahub/core/models.py +6 -6
- infrahub/core/node/__init__.py +157 -58
- infrahub/core/node/base.py +9 -5
- infrahub/core/node/create.py +7 -3
- infrahub/core/node/delete_validator.py +1 -1
- infrahub/core/node/standard.py +100 -14
- infrahub/core/order.py +30 -0
- infrahub/core/property.py +0 -1
- infrahub/core/protocols.py +1 -0
- infrahub/core/protocols_base.py +10 -2
- infrahub/core/query/__init__.py +5 -3
- infrahub/core/query/attribute.py +164 -49
- infrahub/core/query/branch.py +58 -70
- infrahub/core/query/delete.py +1 -1
- infrahub/core/query/diff.py +7 -7
- infrahub/core/query/ipam.py +104 -43
- infrahub/core/query/node.py +1072 -281
- infrahub/core/query/relationship.py +531 -325
- infrahub/core/query/resource_manager.py +107 -18
- infrahub/core/query/standard_node.py +25 -5
- infrahub/core/query/utils.py +2 -4
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/constraints/peer_kind.py +1 -1
- infrahub/core/relationship/constraints/peer_parent.py +1 -1
- infrahub/core/relationship/constraints/peer_relatives.py +1 -1
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/relationship/constraints/profiles_removal.py +168 -0
- infrahub/core/relationship/model.py +293 -139
- infrahub/core/schema/attribute_schema.py +2 -2
- infrahub/core/schema/basenode_schema.py +3 -0
- infrahub/core/schema/definitions/core/__init__.py +8 -2
- infrahub/core/schema/definitions/core/account.py +10 -10
- infrahub/core/schema/definitions/core/artifact.py +14 -8
- infrahub/core/schema/definitions/core/check.py +10 -4
- infrahub/core/schema/definitions/core/generator.py +26 -6
- infrahub/core/schema/definitions/core/graphql_query.py +1 -1
- infrahub/core/schema/definitions/core/group.py +9 -2
- infrahub/core/schema/definitions/core/ipam.py +80 -10
- infrahub/core/schema/definitions/core/menu.py +41 -7
- infrahub/core/schema/definitions/core/permission.py +16 -2
- infrahub/core/schema/definitions/core/profile.py +16 -2
- infrahub/core/schema/definitions/core/propose_change.py +24 -4
- infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
- infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
- infrahub/core/schema/definitions/core/repository.py +10 -0
- infrahub/core/schema/definitions/core/resource_pool.py +8 -1
- infrahub/core/schema/definitions/core/template.py +19 -2
- infrahub/core/schema/definitions/core/transform.py +11 -5
- infrahub/core/schema/definitions/core/webhook.py +27 -9
- infrahub/core/schema/manager.py +63 -43
- infrahub/core/schema/relationship_schema.py +6 -2
- infrahub/core/schema/schema_branch.py +48 -10
- infrahub/core/task/task.py +4 -2
- infrahub/core/utils.py +3 -25
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/attribute/choices.py +1 -1
- infrahub/core/validators/attribute/enum.py +1 -1
- infrahub/core/validators/attribute/kind.py +1 -1
- infrahub/core/validators/attribute/length.py +1 -1
- infrahub/core/validators/attribute/min_max.py +1 -1
- infrahub/core/validators/attribute/number_pool.py +1 -1
- infrahub/core/validators/attribute/optional.py +1 -1
- infrahub/core/validators/attribute/regex.py +1 -1
- infrahub/core/validators/determiner.py +3 -3
- infrahub/core/validators/node/attribute.py +1 -1
- infrahub/core/validators/node/relationship.py +1 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/database/__init__.py +4 -4
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
- infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
- infrahub/dependencies/registry.py +2 -0
- infrahub/display_labels/tasks.py +12 -3
- infrahub/git/integrator.py +18 -18
- infrahub/git/tasks.py +1 -1
- infrahub/git/utils.py +1 -1
- infrahub/graphql/constants.py +3 -0
- infrahub/graphql/context.py +1 -1
- infrahub/graphql/field_extractor.py +1 -1
- infrahub/graphql/initialization.py +11 -0
- infrahub/graphql/loaders/account.py +134 -0
- infrahub/graphql/loaders/node.py +5 -12
- infrahub/graphql/loaders/peers.py +5 -7
- infrahub/graphql/manager.py +175 -21
- infrahub/graphql/metadata.py +91 -0
- infrahub/graphql/mutations/account.py +6 -6
- infrahub/graphql/mutations/attribute.py +0 -2
- infrahub/graphql/mutations/branch.py +9 -5
- infrahub/graphql/mutations/computed_attribute.py +1 -1
- infrahub/graphql/mutations/display_label.py +1 -1
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/ipam.py +4 -6
- infrahub/graphql/mutations/main.py +9 -4
- infrahub/graphql/mutations/profile.py +16 -22
- infrahub/graphql/mutations/proposed_change.py +4 -4
- infrahub/graphql/mutations/relationship.py +40 -10
- infrahub/graphql/mutations/repository.py +14 -12
- infrahub/graphql/mutations/schema.py +2 -2
- infrahub/graphql/order.py +14 -0
- infrahub/graphql/queries/branch.py +62 -6
- infrahub/graphql/queries/resource_manager.py +25 -24
- infrahub/graphql/resolvers/account_metadata.py +84 -0
- infrahub/graphql/resolvers/ipam.py +6 -8
- infrahub/graphql/resolvers/many_relationship.py +77 -35
- infrahub/graphql/resolvers/resolver.py +59 -14
- infrahub/graphql/resolvers/single_relationship.py +87 -23
- infrahub/graphql/subscription/graphql_query.py +2 -0
- infrahub/graphql/types/__init__.py +0 -1
- infrahub/graphql/types/attribute.py +10 -5
- infrahub/graphql/types/branch.py +40 -53
- infrahub/graphql/types/enums.py +3 -0
- infrahub/graphql/types/metadata.py +28 -0
- infrahub/graphql/types/node.py +22 -2
- infrahub/graphql/types/relationship.py +10 -2
- infrahub/graphql/types/standard_node.py +12 -7
- infrahub/hfid/tasks.py +12 -3
- infrahub/lock.py +7 -0
- infrahub/menu/repository.py +1 -1
- infrahub/patch/queries/base.py +1 -1
- infrahub/pools/number.py +1 -8
- infrahub/profiles/gather.py +56 -0
- infrahub/profiles/mandatory_fields_checker.py +116 -0
- infrahub/profiles/models.py +66 -0
- infrahub/profiles/node_applier.py +154 -13
- infrahub/profiles/queries/get_profile_data.py +143 -31
- infrahub/profiles/tasks.py +79 -27
- infrahub/profiles/triggers.py +22 -0
- infrahub/proposed_change/action_checker.py +1 -1
- infrahub/proposed_change/tasks.py +4 -1
- infrahub/services/__init__.py +1 -1
- infrahub/services/adapters/cache/nats.py +1 -1
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/tasks/artifact.py +1 -0
- infrahub/transformations/tasks.py +2 -2
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +1 -0
- infrahub/trigger/setup.py +3 -3
- infrahub/trigger/tasks.py +3 -0
- infrahub/validators/tasks.py +1 -0
- infrahub/webhook/gather.py +1 -1
- infrahub/webhook/models.py +1 -1
- infrahub/webhook/tasks.py +23 -7
- infrahub/workers/dependencies.py +9 -3
- infrahub/workers/infrahub_async.py +13 -4
- infrahub/workflows/catalogue.py +19 -0
- infrahub_sdk/analyzer.py +2 -2
- infrahub_sdk/branch.py +12 -39
- infrahub_sdk/checks.py +4 -4
- infrahub_sdk/client.py +36 -0
- infrahub_sdk/ctl/cli_commands.py +2 -1
- infrahub_sdk/ctl/graphql.py +15 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/enums.py +6 -0
- infrahub_sdk/graphql/renderers.py +21 -0
- infrahub_sdk/graphql/utils.py +85 -0
- infrahub_sdk/node/attribute.py +12 -2
- infrahub_sdk/node/constants.py +12 -0
- infrahub_sdk/node/metadata.py +69 -0
- infrahub_sdk/node/node.py +65 -14
- infrahub_sdk/node/property.py +3 -0
- infrahub_sdk/node/related_node.py +37 -5
- infrahub_sdk/node/relationship.py +18 -1
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/schema/repository.py +1 -2
- infrahub_sdk/transforms.py +2 -2
- infrahub_sdk/types.py +18 -2
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +249 -228
- infrahub_testcontainers/container.py +3 -3
- infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
- infrahub_testcontainers/docker-compose.test.yml +13 -5
- infrahub_testcontainers/models.py +3 -3
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/graphql/models.py +0 -6
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Resolvers for account metadata fields (created_by, updated_by)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
8
|
+
from infrahub.graphql.loaders.account import AccountDataLoader, AccountLoaderParams
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from graphql import GraphQLResolveInfo
|
|
12
|
+
|
|
13
|
+
from infrahub.core.branch.models import Branch
|
|
14
|
+
from infrahub.core.timestamp import Timestamp
|
|
15
|
+
from infrahub.database import InfrahubDatabase
|
|
16
|
+
from infrahub.graphql.initialization import GraphqlContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AccountMetadataResolver:
|
|
20
|
+
"""Resolver class for account metadata fields (created_by, updated_by).
|
|
21
|
+
|
|
22
|
+
This class maintains DataLoader instances to enable batching and caching
|
|
23
|
+
of account lookups across multiple fields within the same request.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._data_loader_instances: dict[AccountLoaderParams, AccountDataLoader] = {}
|
|
28
|
+
|
|
29
|
+
def _get_or_create_loader(
|
|
30
|
+
self,
|
|
31
|
+
db: InfrahubDatabase,
|
|
32
|
+
branch: Branch,
|
|
33
|
+
at: Timestamp | None,
|
|
34
|
+
fields: dict[str, Any],
|
|
35
|
+
) -> AccountDataLoader:
|
|
36
|
+
"""Get an existing loader or create a new one for the given parameters."""
|
|
37
|
+
params = AccountLoaderParams(branch=branch, at=at, fields=fields)
|
|
38
|
+
|
|
39
|
+
if params not in self._data_loader_instances:
|
|
40
|
+
self._data_loader_instances[params] = AccountDataLoader(db=db, params=params)
|
|
41
|
+
|
|
42
|
+
return self._data_loader_instances[params]
|
|
43
|
+
|
|
44
|
+
async def resolve(
|
|
45
|
+
self,
|
|
46
|
+
parent: dict[str, Any],
|
|
47
|
+
info: GraphQLResolveInfo,
|
|
48
|
+
) -> dict[str, Any] | None:
|
|
49
|
+
"""Resolve created_by/updated_by fields in metadata objects.
|
|
50
|
+
|
|
51
|
+
The parent dict should contain {"id": "account-uuid", "__kind__": "CoreAccount"}
|
|
52
|
+
for the field being resolved, or the field value may be None.
|
|
53
|
+
"""
|
|
54
|
+
field_name = info.field_name # "created_by" or "updated_by"
|
|
55
|
+
account_data = parent.get(field_name)
|
|
56
|
+
|
|
57
|
+
if not account_data or not account_data.get("id"):
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
account_id = account_data["id"]
|
|
61
|
+
graphql_context: GraphqlContext = info.context
|
|
62
|
+
|
|
63
|
+
# Extract the fields requested for this account
|
|
64
|
+
fields = extract_graphql_fields(info=info)
|
|
65
|
+
|
|
66
|
+
# Get or create a loader for these parameters
|
|
67
|
+
loader = self._get_or_create_loader(
|
|
68
|
+
db=graphql_context.db,
|
|
69
|
+
branch=graphql_context.branch,
|
|
70
|
+
at=graphql_context.at,
|
|
71
|
+
fields=fields,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return await loader.load(account_id)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def account_metadata_resolver(
|
|
78
|
+
parent: dict[str, Any],
|
|
79
|
+
info: GraphQLResolveInfo,
|
|
80
|
+
) -> dict[str, Any] | None:
|
|
81
|
+
"""Function resolver that delegates to the AccountMetadataResolver on the context."""
|
|
82
|
+
graphql_context: GraphqlContext = info.context
|
|
83
|
+
resolver = graphql_context.account_metadata_resolver
|
|
84
|
+
return await resolver.resolve(parent=parent, info=info)
|
|
@@ -9,7 +9,7 @@ from netaddr import IPSet
|
|
|
9
9
|
from opentelemetry import trace
|
|
10
10
|
|
|
11
11
|
from infrahub.core import registry
|
|
12
|
-
from infrahub.core.constants import InfrahubKind
|
|
12
|
+
from infrahub.core.constants import InfrahubKind, MetadataOptions
|
|
13
13
|
from infrahub.core.ipam.constants import PrefixMemberType
|
|
14
14
|
from infrahub.core.manager import NodeManager
|
|
15
15
|
from infrahub.core.node import Node
|
|
@@ -19,7 +19,7 @@ from infrahub.exceptions import ValidationError
|
|
|
19
19
|
from infrahub.graphql.parser import extract_selection
|
|
20
20
|
from infrahub.graphql.permissions import get_permissions
|
|
21
21
|
|
|
22
|
-
from ..
|
|
22
|
+
from ..order import deserialize_order_input
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
from collections.abc import Sequence
|
|
@@ -31,7 +31,6 @@ if TYPE_CHECKING:
|
|
|
31
31
|
from infrahub.core.schema import NodeSchema
|
|
32
32
|
from infrahub.database import InfrahubDatabase
|
|
33
33
|
from infrahub.graphql.initialization import GraphqlContext
|
|
34
|
-
from infrahub.graphql.models import OrderModel
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def _ip_range_display_label(node: Node) -> str:
|
|
@@ -311,7 +310,7 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
|
|
|
311
310
|
info: GraphQLResolveInfo,
|
|
312
311
|
offset: int | None = None,
|
|
313
312
|
limit: int | None = None,
|
|
314
|
-
order:
|
|
313
|
+
order: dict[str, Any] | None = None,
|
|
315
314
|
partial_match: bool = False,
|
|
316
315
|
**kwargs: dict[str, Any],
|
|
317
316
|
) -> dict[str, Any]:
|
|
@@ -324,6 +323,7 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
|
|
|
324
323
|
if not isinstance(schema, GenericSchema) or schema.kind not in [InfrahubKind.IPADDRESS, InfrahubKind.IPPREFIX]:
|
|
325
324
|
raise ValidationError(f"{schema.kind} is not {InfrahubKind.IPADDRESS} or {InfrahubKind.IPPREFIX}")
|
|
326
325
|
|
|
326
|
+
order_model = deserialize_order_input(input_data=order)
|
|
327
327
|
fields = await extract_selection(info=info, schema=schema)
|
|
328
328
|
resolve_available = bool(kwargs.pop("include_available", False))
|
|
329
329
|
kinds_to_filter: list[str] = kwargs.pop("kinds", []) # type: ignore[assignment]
|
|
@@ -399,11 +399,9 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
|
|
|
399
399
|
branch=graphql_context.branch,
|
|
400
400
|
limit=query_limit,
|
|
401
401
|
offset=offset,
|
|
402
|
-
|
|
403
|
-
include_source=True,
|
|
404
|
-
include_owner=True,
|
|
402
|
+
include_metadata=MetadataOptions.LINKED_NODES,
|
|
405
403
|
partial_match=partial_match,
|
|
406
|
-
order=
|
|
404
|
+
order=order_model,
|
|
407
405
|
)
|
|
408
406
|
|
|
409
407
|
if fetch_first_node_context and len(objs) > 2:
|
|
@@ -3,15 +3,20 @@ from typing import TYPE_CHECKING, Any
|
|
|
3
3
|
from graphql import GraphQLResolveInfo
|
|
4
4
|
|
|
5
5
|
from infrahub.core.branch.models import Branch
|
|
6
|
-
from infrahub.core.constants import
|
|
6
|
+
from infrahub.core.constants import (
|
|
7
|
+
BranchSupportType,
|
|
8
|
+
RelationshipHierarchyDirection,
|
|
9
|
+
)
|
|
7
10
|
from infrahub.core.manager import NodeManager
|
|
11
|
+
from infrahub.core.metadata.model import MetadataQueryOptions
|
|
8
12
|
from infrahub.core.query.node import NodeGetHierarchyQuery
|
|
13
|
+
from infrahub.core.relationship import Relationship
|
|
9
14
|
from infrahub.core.schema.node_schema import NodeSchema
|
|
10
15
|
from infrahub.core.schema.relationship_schema import RelationshipSchema
|
|
11
16
|
from infrahub.core.timestamp import Timestamp
|
|
12
17
|
from infrahub.database import InfrahubDatabase
|
|
13
18
|
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
14
|
-
from infrahub.
|
|
19
|
+
from infrahub.graphql.metadata import build_metadata_query_options, get_metadata_options_from_fields
|
|
15
20
|
|
|
16
21
|
from ..loaders.peers import PeerRelationshipsDataLoader, QueryPeerParams
|
|
17
22
|
from ..types import RELATIONS_PROPERTY_MAP, RELATIONS_PROPERTY_MAP_REVERSED
|
|
@@ -68,6 +73,25 @@ class ManyRelationshipResolver:
|
|
|
68
73
|
branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
|
|
69
74
|
)
|
|
70
75
|
|
|
76
|
+
def _build_relationship_meta_response(
|
|
77
|
+
self, relationship: Relationship, metadata_fields: dict[str, Any]
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
data: dict[str, Any] = {}
|
|
80
|
+
for meta_field in metadata_fields.keys():
|
|
81
|
+
if meta_field == "created_at":
|
|
82
|
+
created_at = relationship._get_created_at()
|
|
83
|
+
data["created_at"] = created_at.to_datetime() if created_at else None
|
|
84
|
+
elif meta_field == "created_by":
|
|
85
|
+
account_id = relationship._get_created_by()
|
|
86
|
+
data["created_by"] = {"id": account_id} if account_id else None
|
|
87
|
+
elif meta_field == "updated_at":
|
|
88
|
+
updated_at = relationship._get_updated_at()
|
|
89
|
+
data["updated_at"] = updated_at.to_datetime() if updated_at else None
|
|
90
|
+
elif meta_field == "updated_by":
|
|
91
|
+
account_id = relationship._get_updated_by()
|
|
92
|
+
data["updated_by"] = {"id": account_id} if account_id else None
|
|
93
|
+
return data
|
|
94
|
+
|
|
71
95
|
async def resolve(
|
|
72
96
|
self,
|
|
73
97
|
parent: dict,
|
|
@@ -93,6 +117,10 @@ class ManyRelationshipResolver:
|
|
|
93
117
|
edges = fields.get("edges", {})
|
|
94
118
|
node_fields = edges.get("node", {})
|
|
95
119
|
property_fields = edges.get("properties", {})
|
|
120
|
+
metadata_fields = {
|
|
121
|
+
"node_metadata": edges.get("node_metadata", {}),
|
|
122
|
+
"relationship_metadata": edges.get("relationship_metadata", {}),
|
|
123
|
+
}
|
|
96
124
|
for key, value in property_fields.items():
|
|
97
125
|
mapped_name = RELATIONS_PROPERTY_MAP[key]
|
|
98
126
|
node_fields[mapped_name] = value
|
|
@@ -138,45 +166,71 @@ class ManyRelationshipResolver:
|
|
|
138
166
|
if not node_fields:
|
|
139
167
|
return response
|
|
140
168
|
|
|
169
|
+
include_metadata = build_metadata_query_options(
|
|
170
|
+
node_metadata_fields=metadata_fields.get("node_metadata"),
|
|
171
|
+
relationship_metadata_fields=metadata_fields.get("relationship_metadata"),
|
|
172
|
+
node_fields=node_fields,
|
|
173
|
+
)
|
|
174
|
+
# Add relationship properties metadata to relationship_level
|
|
175
|
+
include_metadata |= MetadataQueryOptions(relationship_level=get_metadata_options_from_fields(property_fields))
|
|
176
|
+
|
|
141
177
|
if offset or limit:
|
|
142
|
-
|
|
178
|
+
relationships = await self._get_entities_simple(
|
|
143
179
|
db=graphql_context.db,
|
|
144
180
|
branch=graphql_context.branch,
|
|
145
181
|
ids=ids,
|
|
146
182
|
at=graphql_context.at,
|
|
147
|
-
related_node_ids=graphql_context.related_node_ids,
|
|
148
183
|
source_kind=source_kind,
|
|
149
184
|
rel_schema=node_rel,
|
|
150
185
|
filters=filters,
|
|
151
186
|
node_fields=node_fields,
|
|
187
|
+
include_metadata=include_metadata,
|
|
152
188
|
offset=offset,
|
|
153
189
|
limit=limit,
|
|
154
190
|
)
|
|
155
191
|
else:
|
|
156
|
-
|
|
192
|
+
relationships = await self._get_entities_with_data_loader(
|
|
157
193
|
db=graphql_context.db,
|
|
158
194
|
branch=graphql_context.branch,
|
|
159
195
|
ids=ids,
|
|
160
196
|
at=graphql_context.at,
|
|
161
|
-
related_node_ids=graphql_context.related_node_ids,
|
|
162
197
|
source_kind=source_kind,
|
|
163
198
|
rel_schema=node_rel,
|
|
164
199
|
filters=filters,
|
|
165
200
|
node_fields=node_fields,
|
|
201
|
+
include_metadata=include_metadata,
|
|
166
202
|
)
|
|
167
203
|
|
|
168
|
-
if not
|
|
204
|
+
if not relationships:
|
|
169
205
|
return response
|
|
170
206
|
|
|
171
207
|
entries = []
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
async with graphql_context.db.start_session(read_only=True) as db:
|
|
209
|
+
for rel in relationships:
|
|
210
|
+
node = await rel.to_graphql(
|
|
211
|
+
db=db,
|
|
212
|
+
fields=node_fields,
|
|
213
|
+
related_node_ids=graphql_context.related_node_ids,
|
|
214
|
+
)
|
|
215
|
+
entry: dict[str, dict[str, Any]] = {"node": {}, "properties": {}}
|
|
216
|
+
for key, mapped in RELATIONS_PROPERTY_MAP_REVERSED.items():
|
|
217
|
+
value = node.pop(key, None)
|
|
218
|
+
if value:
|
|
219
|
+
entry["properties"][mapped] = value
|
|
220
|
+
entry["node"] = node
|
|
221
|
+
|
|
222
|
+
if metadata_fields.get("node_metadata"):
|
|
223
|
+
peer = await rel.get_peer(db=db)
|
|
224
|
+
if peer:
|
|
225
|
+
entry["node_metadata"] = await peer._build_meta_response("node_metadata", edges)
|
|
226
|
+
|
|
227
|
+
if metadata_fields.get("relationship_metadata"):
|
|
228
|
+
entry["relationship_metadata"] = self._build_relationship_meta_response(
|
|
229
|
+
relationship=rel,
|
|
230
|
+
metadata_fields=metadata_fields["relationship_metadata"],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
entries.append(entry)
|
|
180
234
|
|
|
181
235
|
response["edges"] = entries
|
|
182
236
|
return response
|
|
@@ -187,17 +241,14 @@ class ManyRelationshipResolver:
|
|
|
187
241
|
branch: Branch,
|
|
188
242
|
ids: list[str],
|
|
189
243
|
at: Timestamp | None,
|
|
190
|
-
related_node_ids: set[str] | None,
|
|
191
244
|
source_kind: str,
|
|
192
245
|
rel_schema: RelationshipSchema,
|
|
193
246
|
filters: dict[str, Any],
|
|
194
247
|
node_fields: dict[str, Any],
|
|
248
|
+
include_metadata: MetadataQueryOptions,
|
|
195
249
|
offset: int | None = None,
|
|
196
250
|
limit: int | None = None,
|
|
197
|
-
) -> list[
|
|
198
|
-
include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
|
|
199
|
-
include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
|
|
200
|
-
|
|
251
|
+
) -> list[Relationship] | None:
|
|
201
252
|
async with db.start_session(read_only=True) as dbs:
|
|
202
253
|
objs = await NodeManager.query_peers(
|
|
203
254
|
db=dbs,
|
|
@@ -212,12 +263,11 @@ class ManyRelationshipResolver:
|
|
|
212
263
|
branch=branch,
|
|
213
264
|
branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
|
|
214
265
|
fetch_peers=True,
|
|
215
|
-
|
|
216
|
-
include_owner=include_owner,
|
|
266
|
+
include_metadata=include_metadata,
|
|
217
267
|
)
|
|
218
268
|
if not objs:
|
|
219
269
|
return None
|
|
220
|
-
return
|
|
270
|
+
return objs
|
|
221
271
|
|
|
222
272
|
async def _get_entities_with_data_loader(
|
|
223
273
|
self,
|
|
@@ -225,18 +275,15 @@ class ManyRelationshipResolver:
|
|
|
225
275
|
branch: Branch,
|
|
226
276
|
ids: list[str],
|
|
227
277
|
at: Timestamp | None,
|
|
228
|
-
related_node_ids: set[str] | None,
|
|
229
278
|
source_kind: str,
|
|
230
279
|
rel_schema: RelationshipSchema,
|
|
231
280
|
filters: dict[str, Any],
|
|
232
281
|
node_fields: dict[str, Any],
|
|
233
|
-
|
|
282
|
+
include_metadata: MetadataQueryOptions,
|
|
283
|
+
) -> list[Relationship] | None:
|
|
234
284
|
if node_fields and "hfid" in node_fields:
|
|
235
285
|
node_fields["human_friendly_id"] = None
|
|
236
286
|
|
|
237
|
-
include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
|
|
238
|
-
include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
|
|
239
|
-
|
|
240
287
|
query_params = QueryPeerParams(
|
|
241
288
|
branch=branch,
|
|
242
289
|
source_kind=source_kind,
|
|
@@ -245,8 +292,7 @@ class ManyRelationshipResolver:
|
|
|
245
292
|
fields=node_fields,
|
|
246
293
|
at=at,
|
|
247
294
|
branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
|
|
248
|
-
|
|
249
|
-
include_owner=include_owner,
|
|
295
|
+
include_metadata=include_metadata,
|
|
250
296
|
)
|
|
251
297
|
if query_params in self._data_loader_instances:
|
|
252
298
|
loader = self._data_loader_instances[query_params]
|
|
@@ -259,8 +305,4 @@ class ManyRelationshipResolver:
|
|
|
259
305
|
all_peer_rels.extend(node_peer_rels)
|
|
260
306
|
if not all_peer_rels:
|
|
261
307
|
return None
|
|
262
|
-
|
|
263
|
-
return [
|
|
264
|
-
await obj.to_graphql(db=dbs, fields=node_fields, related_node_ids=related_node_ids)
|
|
265
|
-
for obj in all_peer_rels
|
|
266
|
-
]
|
|
308
|
+
return all_peer_rels
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import datetime, timedelta
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
6
|
from graphql.type.definition import GraphQLNonNull
|
|
@@ -7,18 +8,19 @@ from opentelemetry import trace
|
|
|
7
8
|
|
|
8
9
|
from infrahub.core.constants import BranchSupportType, InfrahubKind, RelationshipHierarchyDirection
|
|
9
10
|
from infrahub.core.manager import NodeManager
|
|
11
|
+
from infrahub.core.order import OrderModel
|
|
10
12
|
from infrahub.exceptions import NodeNotFoundError
|
|
11
13
|
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
12
|
-
from infrahub.
|
|
14
|
+
from infrahub.graphql.metadata import build_metadata_query_options
|
|
13
15
|
|
|
14
|
-
from ..
|
|
16
|
+
from ..order import deserialize_order_input
|
|
15
17
|
from ..parser import extract_selection
|
|
16
18
|
from ..permissions import get_permissions
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from graphql import GraphQLResolveInfo
|
|
20
22
|
|
|
21
|
-
from infrahub.core.schema import NodeSchema
|
|
23
|
+
from infrahub.core.schema import MainSchemaTypes, NodeSchema
|
|
22
24
|
from infrahub.graphql.initialization import GraphqlContext
|
|
23
25
|
|
|
24
26
|
|
|
@@ -140,22 +142,63 @@ async def parent_field_name_resolver(parent: dict[str, dict], info: GraphQLResol
|
|
|
140
142
|
return parent[info.field_name]
|
|
141
143
|
|
|
142
144
|
|
|
145
|
+
def _transform_metadata_day_filters(filters: dict[str, Any]) -> dict[str, Any]:
|
|
146
|
+
"""Transform metadata datetime filters with 00:00:00 time into day range filters.
|
|
147
|
+
|
|
148
|
+
When a filter like `node_metadata__created_at="2025-02-03T00:00:00"` has a time
|
|
149
|
+
of exactly midnight, transform it into __after and __before filters to match
|
|
150
|
+
the entire day (inclusive of midnight).
|
|
151
|
+
|
|
152
|
+
If __after or __before filters are already explicitly defined, they will not be
|
|
153
|
+
overwritten by the generated day range filters.
|
|
154
|
+
"""
|
|
155
|
+
result = dict(filters)
|
|
156
|
+
metadata_datetime_fields = ("node_metadata__created_at", "node_metadata__updated_at")
|
|
157
|
+
|
|
158
|
+
for field in metadata_datetime_fields:
|
|
159
|
+
if field not in result:
|
|
160
|
+
continue
|
|
161
|
+
value = result[field]
|
|
162
|
+
if not isinstance(value, datetime):
|
|
163
|
+
continue
|
|
164
|
+
# Check if time is midnight (00:00:00)
|
|
165
|
+
if value.hour == 0 and value.minute == 0 and value.second == 0 and value.microsecond == 0:
|
|
166
|
+
# Remove the exact match filter
|
|
167
|
+
del result[field]
|
|
168
|
+
# Add __after filter with one microsecond before midnight to include objects at exactly midnight
|
|
169
|
+
# Skip if __after is already explicitly defined
|
|
170
|
+
after_key = f"{field}__after"
|
|
171
|
+
if after_key not in result:
|
|
172
|
+
one_microsecond_before = value - timedelta(microseconds=1)
|
|
173
|
+
result[after_key] = one_microsecond_before
|
|
174
|
+
# Add __before filter with next day (exclusive: <)
|
|
175
|
+
# Skip if __before is already explicitly defined
|
|
176
|
+
before_key = f"{field}__before"
|
|
177
|
+
if before_key not in result:
|
|
178
|
+
next_day = value + timedelta(days=1)
|
|
179
|
+
result[before_key] = next_day
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
143
184
|
@trace.get_tracer(__name__).start_as_current_span("default_paginated_list_resolver")
|
|
144
185
|
async def default_paginated_list_resolver(
|
|
145
186
|
root: dict, # noqa: ARG001
|
|
146
187
|
info: GraphQLResolveInfo,
|
|
147
188
|
offset: int | None = None,
|
|
148
189
|
limit: int | None = None,
|
|
149
|
-
order:
|
|
190
|
+
order: dict | None = None,
|
|
150
191
|
partial_match: bool = False,
|
|
151
192
|
**kwargs: dict[str, Any],
|
|
152
193
|
) -> dict[str, Any]:
|
|
153
|
-
schema:
|
|
194
|
+
schema: MainSchemaTypes = (
|
|
154
195
|
info.return_type.of_type.graphene_type._meta.schema
|
|
155
196
|
if isinstance(info.return_type, GraphQLNonNull)
|
|
156
197
|
else info.return_type.graphene_type._meta.schema
|
|
157
198
|
)
|
|
158
199
|
|
|
200
|
+
order_model = deserialize_order_input(input_data=order)
|
|
201
|
+
|
|
159
202
|
fields = await extract_selection(info=info, schema=schema)
|
|
160
203
|
|
|
161
204
|
graphql_context: GraphqlContext = info.context
|
|
@@ -164,9 +207,15 @@ async def default_paginated_list_resolver(
|
|
|
164
207
|
filters = {
|
|
165
208
|
key: value for key, value in kwargs.items() if ("__" in key and value is not None) or key in ("ids", "hfid")
|
|
166
209
|
}
|
|
210
|
+
filters = _transform_metadata_day_filters(filters)
|
|
167
211
|
|
|
168
|
-
edges = fields.get("edges", {})
|
|
212
|
+
edges: dict[str, Any] = fields.get("edges", {})
|
|
169
213
|
node_fields = edges.get("node", {})
|
|
214
|
+
node_metadata_fields: dict[str, Any] = edges.get("node_metadata", {})
|
|
215
|
+
include_metadata = build_metadata_query_options(
|
|
216
|
+
node_metadata_fields=node_metadata_fields,
|
|
217
|
+
node_fields=node_fields,
|
|
218
|
+
)
|
|
170
219
|
if "hfid" in node_fields:
|
|
171
220
|
node_fields["human_friendly_id"] = None
|
|
172
221
|
|
|
@@ -186,23 +235,18 @@ async def default_paginated_list_resolver(
|
|
|
186
235
|
|
|
187
236
|
objs = []
|
|
188
237
|
if edges or "hfid" in filters:
|
|
189
|
-
include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
|
|
190
|
-
include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
|
|
191
|
-
|
|
192
238
|
objs = await NodeManager.query(
|
|
193
239
|
db=db,
|
|
194
240
|
schema=schema,
|
|
195
241
|
filters=filters or None,
|
|
196
242
|
fields=node_fields,
|
|
243
|
+
include_metadata=include_metadata,
|
|
197
244
|
at=graphql_context.at,
|
|
198
245
|
branch=graphql_context.branch,
|
|
199
246
|
limit=limit,
|
|
200
247
|
offset=offset,
|
|
201
|
-
account=graphql_context.account_session,
|
|
202
|
-
include_source=include_source,
|
|
203
|
-
include_owner=include_owner,
|
|
204
248
|
partial_match=partial_match,
|
|
205
|
-
order=
|
|
249
|
+
order=order_model,
|
|
206
250
|
)
|
|
207
251
|
|
|
208
252
|
if "count" in fields:
|
|
@@ -227,7 +271,8 @@ async def default_paginated_list_resolver(
|
|
|
227
271
|
fields=node_fields,
|
|
228
272
|
related_node_ids=graphql_context.related_node_ids,
|
|
229
273
|
permissions=permission_set,
|
|
230
|
-
)
|
|
274
|
+
),
|
|
275
|
+
"node_metadata": await obj._build_meta_response("node_metadata", edges),
|
|
231
276
|
}
|
|
232
277
|
for obj in objs
|
|
233
278
|
]
|