infrahub-server 1.6.2__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +12 -9
- 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 +26 -6
- 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 +11 -6
- 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_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +11 -17
- 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 +115 -11
- 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 +6 -3
- 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/app.py +2 -2
- 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/diff/tree.py +5 -5
- 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.2.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +252 -231
- 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.2.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/core/query/node.py
CHANGED
|
@@ -5,26 +5,39 @@ from collections import defaultdict
|
|
|
5
5
|
from copy import copy
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from dataclasses import field as dataclass_field
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from typing import TYPE_CHECKING, Any, AsyncIterator, Generator
|
|
10
11
|
|
|
12
|
+
import ujson
|
|
13
|
+
from whenever import ZonedDateTime
|
|
14
|
+
|
|
11
15
|
from infrahub import config
|
|
16
|
+
from infrahub.constants.enums import OrderDirection
|
|
12
17
|
from infrahub.core import registry
|
|
13
18
|
from infrahub.core.constants import (
|
|
14
19
|
GLOBAL_BRANCH_NAME,
|
|
15
20
|
PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
|
|
16
21
|
PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER,
|
|
17
22
|
AttributeDBNodeType,
|
|
23
|
+
MetadataOptions,
|
|
18
24
|
RelationshipDirection,
|
|
19
25
|
RelationshipHierarchyDirection,
|
|
20
26
|
)
|
|
27
|
+
from infrahub.core.order import (
|
|
28
|
+
METADATA_CREATED_AT,
|
|
29
|
+
METADATA_CREATED_BY,
|
|
30
|
+
METADATA_UPDATED_AT,
|
|
31
|
+
METADATA_UPDATED_BY,
|
|
32
|
+
OrderModel,
|
|
33
|
+
)
|
|
21
34
|
from infrahub.core.query import Query, QueryResult, QueryType
|
|
22
35
|
from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
|
|
23
36
|
from infrahub.core.query.utils import find_node_schema
|
|
24
37
|
from infrahub.core.schema.attribute_schema import AttributeSchema
|
|
38
|
+
from infrahub.core.timestamp import Timestamp
|
|
25
39
|
from infrahub.core.utils import build_regex_attrs, extract_field_filters
|
|
26
40
|
from infrahub.exceptions import QueryError
|
|
27
|
-
from infrahub.graphql.models import OrderModel
|
|
28
41
|
|
|
29
42
|
if TYPE_CHECKING:
|
|
30
43
|
from neo4j.graph import Node as Neo4jNode
|
|
@@ -40,18 +53,25 @@ if TYPE_CHECKING:
|
|
|
40
53
|
from infrahub.database import InfrahubDatabase
|
|
41
54
|
|
|
42
55
|
|
|
56
|
+
# Grouped constants for validation/iteration
|
|
57
|
+
METADATA_CREATED_FIELDS = (METADATA_CREATED_AT, METADATA_CREATED_BY)
|
|
58
|
+
METADATA_UPDATED_FIELDS = (METADATA_UPDATED_AT, METADATA_UPDATED_BY)
|
|
59
|
+
NODE_METADATA_PREFIX = "node_metadata__"
|
|
60
|
+
|
|
61
|
+
|
|
43
62
|
@dataclass
|
|
44
63
|
class NodeToProcess:
|
|
45
64
|
schema: NodeSchema | ProfileSchema | TemplateSchema | None
|
|
46
65
|
|
|
66
|
+
labels: list[str]
|
|
47
67
|
node_id: str
|
|
48
68
|
node_uuid: str
|
|
49
|
-
|
|
50
|
-
updated_at: str
|
|
51
|
-
|
|
52
69
|
branch: str
|
|
53
70
|
|
|
54
|
-
|
|
71
|
+
created_at: Timestamp | None = None
|
|
72
|
+
created_by: str | None = None
|
|
73
|
+
updated_at: Timestamp | None = None
|
|
74
|
+
updated_by: str | None = None
|
|
55
75
|
|
|
56
76
|
|
|
57
77
|
@dataclass
|
|
@@ -74,13 +94,16 @@ class AttributeFromDB:
|
|
|
74
94
|
value: Any
|
|
75
95
|
content: Any
|
|
76
96
|
|
|
77
|
-
updated_at: str
|
|
78
|
-
|
|
79
97
|
branch: str
|
|
80
98
|
|
|
81
99
|
is_default: bool
|
|
82
100
|
is_from_profile: bool = dataclass_field(default=False)
|
|
83
101
|
|
|
102
|
+
updated_at: Timestamp | None = None
|
|
103
|
+
updated_by: str | None = None
|
|
104
|
+
created_at: Timestamp | None = None
|
|
105
|
+
created_by: str | None = None
|
|
106
|
+
|
|
84
107
|
node_properties: dict[str, AttributeNodePropertyFromDB] = dataclass_field(default_factory=dict)
|
|
85
108
|
flag_properties: dict[str, bool] = dataclass_field(default_factory=dict)
|
|
86
109
|
|
|
@@ -108,8 +131,6 @@ class NodeQuery(Query):
|
|
|
108
131
|
branch: Branch | None = None,
|
|
109
132
|
**kwargs,
|
|
110
133
|
) -> None:
|
|
111
|
-
# TODO Validate that Node is a valid node
|
|
112
|
-
# Eventually extract the branch from Node as well
|
|
113
134
|
self.node = node
|
|
114
135
|
self.node_id = node_id or id
|
|
115
136
|
self.node_db_id = node_db_id
|
|
@@ -133,6 +154,7 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
133
154
|
|
|
134
155
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002, PLR0915
|
|
135
156
|
at = self.at or self.node._at
|
|
157
|
+
self.params["user_id"] = self.user_id
|
|
136
158
|
self.params["uuid"] = self.node.id
|
|
137
159
|
self.params["branch"] = self.branch.name
|
|
138
160
|
self.params["branch_level"] = self.branch.hierarchy_level
|
|
@@ -188,7 +210,7 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
188
210
|
pass
|
|
189
211
|
except ValueError:
|
|
190
212
|
# Relationship has not been initialized yet, it means the peer does not exist in db yet
|
|
191
|
-
# typically because it will be allocated from a
|
|
213
|
+
# typically because it will be allocated from a resource pool. In that case, the peer
|
|
192
214
|
# will be fetched using `rel.resolve` later.
|
|
193
215
|
pass
|
|
194
216
|
|
|
@@ -220,14 +242,40 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
220
242
|
"namespace": self.node._schema.namespace,
|
|
221
243
|
"branch_support": self.node._schema.branch,
|
|
222
244
|
}
|
|
245
|
+
if self.branch.is_default or self.branch.is_global:
|
|
246
|
+
self.params["node_prop"].update(
|
|
247
|
+
{
|
|
248
|
+
"created_at": at.to_string(),
|
|
249
|
+
"created_by": self.user_id,
|
|
250
|
+
"updated_at": at.to_string(),
|
|
251
|
+
"updated_by": self.user_id,
|
|
252
|
+
}
|
|
253
|
+
)
|
|
223
254
|
self.params["node_branch_prop"] = {
|
|
224
255
|
"branch": self.branch.name,
|
|
225
256
|
"branch_level": self.branch.hierarchy_level,
|
|
226
257
|
"status": "active",
|
|
227
258
|
"from": at.to_string(),
|
|
259
|
+
"from_user_id": self.user_id,
|
|
228
260
|
}
|
|
229
261
|
|
|
230
|
-
|
|
262
|
+
# set all the property strings that we reuse
|
|
263
|
+
# include the create/updated_at/by metadata if on default or global branch
|
|
264
|
+
attr_edge_prop_str = "{ branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at, from_user_id: $user_id }"
|
|
265
|
+
attr_vertex_prop_str = "{ uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support"
|
|
266
|
+
if self.branch.is_default or self.branch.is_global:
|
|
267
|
+
attr_vertex_prop_str += ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
|
|
268
|
+
attr_vertex_prop_str += " }"
|
|
269
|
+
|
|
270
|
+
rel_edge_prop_str = "{ branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at, from_user_id: $user_id }"
|
|
271
|
+
rel_edge_prop_str_hierarchy = (
|
|
272
|
+
"{ branch: rel.branch, branch_level: rel.branch_level, "
|
|
273
|
+
"status: rel.status, hierarchy: rel.hierarchical, from: $at, from_user_id: $user_id }"
|
|
274
|
+
)
|
|
275
|
+
rel_vertex_prop_str = "{ uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support"
|
|
276
|
+
if self.branch.is_default or self.branch.is_global:
|
|
277
|
+
rel_vertex_prop_str += ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
|
|
278
|
+
rel_vertex_prop_str += " }"
|
|
231
279
|
|
|
232
280
|
iphost_prop = {
|
|
233
281
|
"value": "attr.content.value",
|
|
@@ -270,102 +318,102 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
270
318
|
LIMIT 1
|
|
271
319
|
}
|
|
272
320
|
CALL (n, attr, av) {
|
|
273
|
-
CREATE (a:Attribute
|
|
274
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
275
|
-
CREATE (a)-[:HAS_VALUE
|
|
321
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
322
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
323
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
276
324
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
277
|
-
|
|
278
|
-
WITH a, ip, iv
|
|
325
|
+
WITH a, ip
|
|
279
326
|
LIMIT 1
|
|
280
|
-
CREATE (a)-[:IS_PROTECTED
|
|
281
|
-
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
327
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
282
328
|
FOREACH ( prop IN attr.source_prop |
|
|
283
329
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
284
|
-
CREATE (a)-[:HAS_SOURCE
|
|
330
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
285
331
|
)
|
|
286
332
|
FOREACH ( prop IN attr.owner_prop |
|
|
287
333
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
288
|
-
CREATE (a)-[:HAS_OWNER
|
|
334
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
289
335
|
)
|
|
290
|
-
}"""
|
|
336
|
+
}""" % {"attr_edge": attr_edge_prop_str, "attr_vertex": attr_vertex_prop_str}
|
|
291
337
|
|
|
292
338
|
attrs_indexed_query = """
|
|
293
339
|
WITH distinct n
|
|
294
340
|
UNWIND $attrs_indexed AS attr
|
|
295
341
|
CALL (n, attr) {
|
|
296
|
-
CREATE (a:Attribute
|
|
297
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
342
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
343
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
298
344
|
MERGE (av:AttributeValue:AttributeValueIndexed { value: attr.content.value, is_default: attr.content.is_default })
|
|
299
345
|
WITH av, a
|
|
300
346
|
LIMIT 1
|
|
301
|
-
CREATE (a)-[:HAS_VALUE
|
|
347
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
302
348
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
303
|
-
|
|
304
|
-
CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
|
|
305
|
-
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
349
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
306
350
|
FOREACH ( prop IN attr.source_prop |
|
|
307
351
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
308
|
-
CREATE (a)-[:HAS_SOURCE
|
|
352
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
309
353
|
)
|
|
310
354
|
FOREACH ( prop IN attr.owner_prop |
|
|
311
355
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
312
|
-
CREATE (a)-[:HAS_OWNER
|
|
356
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
313
357
|
)
|
|
314
|
-
}"""
|
|
358
|
+
}""" % {"attr_edge": attr_edge_prop_str, "attr_vertex": attr_vertex_prop_str}
|
|
315
359
|
|
|
316
360
|
attrs_iphost_query = """
|
|
317
361
|
WITH distinct n
|
|
318
362
|
UNWIND $attrs_iphost AS attr
|
|
319
363
|
CALL (n, attr) {
|
|
320
|
-
CREATE (a:Attribute
|
|
321
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
364
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
365
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
322
366
|
MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPHost { %(iphost_prop)s })
|
|
323
367
|
WITH attr, av, a
|
|
324
368
|
LIMIT 1
|
|
325
|
-
CREATE (a)-[:HAS_VALUE
|
|
369
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
326
370
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
327
|
-
|
|
328
|
-
WITH a, ip, iv
|
|
371
|
+
WITH a, ip
|
|
329
372
|
LIMIT 1
|
|
330
|
-
CREATE (a)-[:IS_PROTECTED
|
|
331
|
-
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
373
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
332
374
|
FOREACH ( prop IN attr.source_prop |
|
|
333
375
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
334
|
-
CREATE (a)-[:HAS_SOURCE
|
|
376
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
335
377
|
)
|
|
336
378
|
FOREACH ( prop IN attr.owner_prop |
|
|
337
379
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
338
|
-
CREATE (a)-[:HAS_OWNER
|
|
380
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
339
381
|
)
|
|
340
382
|
}
|
|
341
|
-
""" % {
|
|
383
|
+
""" % {
|
|
384
|
+
"iphost_prop": ", ".join(iphost_prop_list),
|
|
385
|
+
"attr_edge": attr_edge_prop_str,
|
|
386
|
+
"attr_vertex": attr_vertex_prop_str,
|
|
387
|
+
}
|
|
342
388
|
|
|
343
389
|
attrs_ipnetwork_query = """
|
|
344
390
|
WITH distinct n
|
|
345
391
|
UNWIND $attrs_ipnetwork AS attr
|
|
346
392
|
CALL (n, attr) {
|
|
347
|
-
CREATE (a:Attribute
|
|
348
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
393
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
394
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
349
395
|
MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPNetwork { %(ipnetwork_prop)s })
|
|
350
396
|
WITH attr, av, a
|
|
351
397
|
LIMIT 1
|
|
352
|
-
CREATE (a)-[:HAS_VALUE
|
|
398
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
353
399
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
354
|
-
|
|
355
|
-
WITH a, ip, iv
|
|
400
|
+
WITH a, ip
|
|
356
401
|
LIMIT 1
|
|
357
|
-
CREATE (a)-[:IS_PROTECTED
|
|
358
|
-
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
402
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
359
403
|
FOREACH ( prop IN attr.source_prop |
|
|
360
404
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
361
|
-
CREATE (a)-[:HAS_SOURCE
|
|
405
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
362
406
|
)
|
|
363
407
|
FOREACH ( prop IN attr.owner_prop |
|
|
364
408
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
365
|
-
CREATE (a)-[:HAS_OWNER
|
|
409
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
366
410
|
)
|
|
367
411
|
}
|
|
368
|
-
""" % {
|
|
412
|
+
""" % {
|
|
413
|
+
"ipnetwork_prop": ", ".join(ipnetwork_prop_list),
|
|
414
|
+
"attr_edge": attr_edge_prop_str,
|
|
415
|
+
"attr_vertex": attr_vertex_prop_str,
|
|
416
|
+
}
|
|
369
417
|
|
|
370
418
|
deepest_branch = await registry.get_branch(db=db, branch=deepest_branch_name)
|
|
371
419
|
branch_filter, branch_params = deepest_branch.get_query_filter_path(at=self.at)
|
|
@@ -409,75 +457,84 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
409
457
|
UNWIND $rels_bidir AS rel
|
|
410
458
|
%(dest_node_subquery)s
|
|
411
459
|
CALL (n, rel, dest_node) {
|
|
412
|
-
CREATE (rl:Relationship
|
|
413
|
-
CREATE (n)-[:IS_RELATED %(
|
|
414
|
-
CREATE (dest_node)-[:IS_RELATED %(
|
|
460
|
+
CREATE (rl:Relationship %(rel_vertex)s)
|
|
461
|
+
CREATE (n)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
462
|
+
CREATE (dest_node)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
415
463
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
416
|
-
|
|
417
|
-
WITH rl, ip, iv
|
|
464
|
+
WITH rl, ip
|
|
418
465
|
LIMIT 1
|
|
419
|
-
CREATE (rl)-[:IS_PROTECTED
|
|
420
|
-
CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
|
|
466
|
+
CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
|
|
421
467
|
FOREACH ( prop IN rel.source_prop |
|
|
422
468
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
423
|
-
CREATE (rl)-[:HAS_SOURCE
|
|
469
|
+
CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
|
|
424
470
|
)
|
|
425
471
|
FOREACH ( prop IN rel.owner_prop |
|
|
426
472
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
427
|
-
CREATE (rl)-[:HAS_OWNER
|
|
473
|
+
CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
|
|
428
474
|
)
|
|
429
475
|
}
|
|
430
|
-
""" % {
|
|
476
|
+
""" % {
|
|
477
|
+
"rel_edge": rel_edge_prop_str,
|
|
478
|
+
"rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
|
|
479
|
+
"rel_vertex": rel_vertex_prop_str,
|
|
480
|
+
"dest_node_subquery": dest_node_subquery,
|
|
481
|
+
}
|
|
431
482
|
|
|
432
483
|
rels_out_query = """
|
|
433
484
|
WITH distinct n
|
|
434
485
|
UNWIND $rels_out AS rel
|
|
435
486
|
%(dest_node_subquery)s
|
|
436
487
|
CALL (n, rel, dest_node) {
|
|
437
|
-
CREATE (rl:Relationship
|
|
438
|
-
CREATE (n)-[:IS_RELATED %(
|
|
439
|
-
CREATE (dest_node)<-[:IS_RELATED %(
|
|
488
|
+
CREATE (rl:Relationship %(rel_vertex)s)
|
|
489
|
+
CREATE (n)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
490
|
+
CREATE (dest_node)<-[:IS_RELATED %(rel_edge_hierarchy)s ]-(rl)
|
|
440
491
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
441
|
-
|
|
442
|
-
WITH rl, ip, iv
|
|
492
|
+
WITH rl, ip
|
|
443
493
|
LIMIT 1
|
|
444
|
-
CREATE (rl)-[:IS_PROTECTED
|
|
445
|
-
CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
|
|
494
|
+
CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
|
|
446
495
|
FOREACH ( prop IN rel.source_prop |
|
|
447
496
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
448
|
-
CREATE (rl)-[:HAS_SOURCE
|
|
497
|
+
CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
|
|
449
498
|
)
|
|
450
499
|
FOREACH ( prop IN rel.owner_prop |
|
|
451
500
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
452
|
-
CREATE (rl)-[:HAS_OWNER
|
|
501
|
+
CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
|
|
453
502
|
)
|
|
454
503
|
}
|
|
455
|
-
""" % {
|
|
504
|
+
""" % {
|
|
505
|
+
"rel_edge": rel_edge_prop_str,
|
|
506
|
+
"rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
|
|
507
|
+
"rel_vertex": rel_vertex_prop_str,
|
|
508
|
+
"dest_node_subquery": dest_node_subquery,
|
|
509
|
+
}
|
|
456
510
|
|
|
457
511
|
rels_in_query = """
|
|
458
512
|
WITH distinct n
|
|
459
513
|
UNWIND $rels_in AS rel
|
|
460
514
|
%(dest_node_subquery)s
|
|
461
515
|
CALL (n, rel, dest_node) {
|
|
462
|
-
CREATE (rl:Relationship
|
|
463
|
-
CREATE (n)<-[:IS_RELATED %(
|
|
464
|
-
CREATE (dest_node)-[:IS_RELATED %(
|
|
516
|
+
CREATE (rl:Relationship %(rel_vertex)s)
|
|
517
|
+
CREATE (n)<-[:IS_RELATED %(rel_edge_hierarchy)s ]-(rl)
|
|
518
|
+
CREATE (dest_node)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
465
519
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
466
|
-
|
|
467
|
-
WITH rl, ip, iv
|
|
520
|
+
WITH rl, ip
|
|
468
521
|
LIMIT 1
|
|
469
|
-
CREATE (rl)-[:IS_PROTECTED
|
|
470
|
-
CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
|
|
522
|
+
CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
|
|
471
523
|
FOREACH ( prop IN rel.source_prop |
|
|
472
524
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
473
|
-
CREATE (rl)-[:HAS_SOURCE
|
|
525
|
+
CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
|
|
474
526
|
)
|
|
475
527
|
FOREACH ( prop IN rel.owner_prop |
|
|
476
528
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
477
|
-
CREATE (rl)-[:HAS_OWNER
|
|
529
|
+
CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
|
|
478
530
|
)
|
|
479
531
|
}
|
|
480
|
-
""" % {
|
|
532
|
+
""" % {
|
|
533
|
+
"rel_edge": rel_edge_prop_str,
|
|
534
|
+
"rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
|
|
535
|
+
"rel_vertex": rel_vertex_prop_str,
|
|
536
|
+
"dest_node_subquery": dest_node_subquery,
|
|
537
|
+
}
|
|
481
538
|
|
|
482
539
|
query = f"""
|
|
483
540
|
MATCH (root:Root)
|
|
@@ -526,48 +583,83 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
526
583
|
|
|
527
584
|
class NodeDeleteQuery(NodeQuery):
|
|
528
585
|
name = "node_delete"
|
|
529
|
-
|
|
530
586
|
type: QueryType = QueryType.WRITE
|
|
531
|
-
|
|
532
|
-
raise_error_if_empty
|
|
587
|
+
insert_return = False
|
|
588
|
+
raise_error_if_empty = False
|
|
533
589
|
|
|
534
590
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
591
|
+
self.params["user_id"] = self.user_id
|
|
535
592
|
self.params["uuid"] = self.node_id
|
|
536
593
|
self.params["branch"] = self.branch.name
|
|
537
594
|
self.params["branch_level"] = self.branch.hierarchy_level
|
|
595
|
+
self.params["at"] = self.at.to_string()
|
|
538
596
|
|
|
539
597
|
if self.branch.is_global or self.branch.is_default:
|
|
598
|
+
# update the updated_at/by metadata on the Node if we're on the global or default branch
|
|
540
599
|
node_query_match = """
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
600
|
+
MATCH (n:Node { uuid: $uuid })-[r:IS_PART_OF { branch_level: 1, status: "active" }]->(:Root)
|
|
601
|
+
WHERE r.to IS NULL
|
|
602
|
+
OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
|
|
603
|
+
WHERE delete_edge.from <= $at
|
|
604
|
+
WITH n, r
|
|
605
|
+
WHERE delete_edge IS NULL
|
|
606
|
+
SET n.updated_at = $at, n.updated_by = $user_id
|
|
607
|
+
WITH n, r
|
|
545
608
|
"""
|
|
546
609
|
else:
|
|
547
610
|
node_filter, node_filter_params = self.branch.get_query_filter_path(at=self.at, variable_name="r")
|
|
548
611
|
node_query_match = """
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
612
|
+
MATCH (n:Node { uuid: $uuid })
|
|
613
|
+
CALL (n) {
|
|
614
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
615
|
+
WHERE %(node_filter)s
|
|
616
|
+
RETURN r
|
|
617
|
+
ORDER BY r.from DESC
|
|
618
|
+
LIMIT 1
|
|
619
|
+
}
|
|
620
|
+
WITH n, r
|
|
621
|
+
WHERE r.status = "active"
|
|
558
622
|
""" % {"node_filter": node_filter}
|
|
559
623
|
self.params.update(node_filter_params)
|
|
560
624
|
self.add_to_query(node_query_match)
|
|
561
625
|
|
|
626
|
+
# set the to time/user_id if the active IS_PART_OF edge is on this branch
|
|
562
627
|
query = """
|
|
563
|
-
|
|
564
|
-
|
|
628
|
+
MATCH (root:Root)
|
|
629
|
+
LIMIT 1
|
|
630
|
+
CREATE (n)-[delete_edge:IS_PART_OF { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]->(root)
|
|
631
|
+
WITH r
|
|
632
|
+
WHERE r.branch = $branch
|
|
633
|
+
SET r.to = $at
|
|
634
|
+
SET r.to_user_id = $user_id
|
|
565
635
|
"""
|
|
636
|
+
self.add_to_query(query)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class NodeUpdateMetadataQuery(NodeQuery):
|
|
640
|
+
name = "node_update_metadata"
|
|
641
|
+
type: QueryType = QueryType.WRITE
|
|
642
|
+
insert_return = False
|
|
643
|
+
raise_error_if_empty = False
|
|
566
644
|
|
|
645
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
646
|
+
if not self.branch.is_default and not self.branch.is_global:
|
|
647
|
+
raise ValueError("NodeUpdateMetadataQuery can only be used on the default or global branch")
|
|
648
|
+
self.params["uuid"] = self.node_id
|
|
649
|
+
self.params["branch"] = self.branch.name
|
|
567
650
|
self.params["at"] = self.at.to_string()
|
|
651
|
+
self.params["user_id"] = self.user_id
|
|
568
652
|
|
|
653
|
+
query = """
|
|
654
|
+
MATCH (n:Node { uuid: $uuid })-[r:IS_PART_OF { branch_level: 1, status: "active" }]->(:Root)
|
|
655
|
+
WHERE r.to IS NULL
|
|
656
|
+
OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
|
|
657
|
+
WHERE delete_edge.from <= $at
|
|
658
|
+
WITH n, r
|
|
659
|
+
WHERE delete_edge IS NULL
|
|
660
|
+
SET n.updated_at = $at, n.updated_by = $user_id
|
|
661
|
+
"""
|
|
569
662
|
self.add_to_query(query)
|
|
570
|
-
self.return_labels = ["n"]
|
|
571
663
|
|
|
572
664
|
|
|
573
665
|
class NodeCheckIDQuery(Query):
|
|
@@ -579,7 +671,7 @@ class NodeCheckIDQuery(Query):
|
|
|
579
671
|
self,
|
|
580
672
|
node_id: str,
|
|
581
673
|
**kwargs,
|
|
582
|
-
):
|
|
674
|
+
) -> None:
|
|
583
675
|
self.node_id = node_id
|
|
584
676
|
super().__init__(**kwargs)
|
|
585
677
|
|
|
@@ -603,30 +695,140 @@ class NodeListGetAttributeQuery(Query):
|
|
|
603
695
|
"HAS_OWNER": ("rel_owner", "owner"),
|
|
604
696
|
"HAS_SOURCE": ("rel_source", "source"),
|
|
605
697
|
"IS_PROTECTED": ("rel_isp", "isp"),
|
|
606
|
-
"IS_VISIBLE": ("rel_isv", "isv"),
|
|
607
698
|
}
|
|
608
699
|
|
|
609
700
|
def __init__(
|
|
610
701
|
self,
|
|
611
702
|
ids: list[str],
|
|
612
703
|
fields: dict | None = None,
|
|
613
|
-
|
|
614
|
-
include_owner: bool = False,
|
|
615
|
-
account=None,
|
|
704
|
+
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
616
705
|
**kwargs,
|
|
617
|
-
):
|
|
618
|
-
self.account = account
|
|
706
|
+
) -> None:
|
|
619
707
|
self.ids = ids
|
|
620
708
|
self.fields = fields
|
|
621
|
-
self.
|
|
622
|
-
self.include_owner = include_owner
|
|
623
|
-
|
|
709
|
+
self.include_metadata = include_metadata
|
|
624
710
|
super().__init__(order_by=["n.uuid", "a.name"], **kwargs)
|
|
625
711
|
|
|
712
|
+
@property
|
|
713
|
+
def _include_source(self) -> bool:
|
|
714
|
+
return bool(self.include_metadata & MetadataOptions.SOURCE)
|
|
715
|
+
|
|
716
|
+
@property
|
|
717
|
+
def _include_owner(self) -> bool:
|
|
718
|
+
return bool(self.include_metadata & MetadataOptions.OWNER)
|
|
719
|
+
|
|
720
|
+
@property
|
|
721
|
+
def _include_updated_metadata(self) -> bool:
|
|
722
|
+
return bool(self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY))
|
|
723
|
+
|
|
724
|
+
@property
|
|
725
|
+
def _include_created_metadata(self) -> bool:
|
|
726
|
+
return bool(self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY))
|
|
727
|
+
|
|
728
|
+
def _add_source_to_query(self, branch_filter_str: str) -> None:
|
|
729
|
+
if not self._include_source:
|
|
730
|
+
return
|
|
731
|
+
source_query = """
|
|
732
|
+
CALL (a) {
|
|
733
|
+
OPTIONAL MATCH (a)-[rel_source:HAS_SOURCE]-(source)
|
|
734
|
+
WHERE all(r IN [rel_source] WHERE ( %(branch_filter)s ))
|
|
735
|
+
RETURN source, rel_source
|
|
736
|
+
ORDER BY rel_source.branch_level DESC, rel_source.from DESC, rel_source.status ASC
|
|
737
|
+
LIMIT 1
|
|
738
|
+
}
|
|
739
|
+
WITH *,
|
|
740
|
+
CASE WHEN rel_source.status = "active" THEN source ELSE NULL END AS source,
|
|
741
|
+
CASE WHEN rel_source.status = "active" THEN rel_source ELSE NULL END AS rel_source
|
|
742
|
+
""" % {"branch_filter": branch_filter_str}
|
|
743
|
+
self.add_to_query(source_query)
|
|
744
|
+
self.return_labels.extend(["source", "rel_source"])
|
|
745
|
+
|
|
746
|
+
def _add_owner_to_query(self, branch_filter_str: str) -> None:
|
|
747
|
+
if not self._include_owner:
|
|
748
|
+
return
|
|
749
|
+
owner_query = """
|
|
750
|
+
CALL (a) {
|
|
751
|
+
OPTIONAL MATCH (a)-[rel_owner:HAS_OWNER]-(owner)
|
|
752
|
+
WHERE all(r IN [rel_owner] WHERE ( %(branch_filter)s ))
|
|
753
|
+
RETURN owner, rel_owner
|
|
754
|
+
ORDER BY rel_owner.branch_level DESC, rel_owner.from DESC, rel_owner.status ASC
|
|
755
|
+
LIMIT 1
|
|
756
|
+
}
|
|
757
|
+
WITH *,
|
|
758
|
+
CASE WHEN rel_owner.status = "active" THEN owner ELSE NULL END AS owner,
|
|
759
|
+
CASE WHEN rel_owner.status = "active" THEN rel_owner ELSE NULL END AS rel_owner
|
|
760
|
+
""" % {"branch_filter": branch_filter_str}
|
|
761
|
+
self.add_to_query(owner_query)
|
|
762
|
+
self.return_labels.extend(["owner", "rel_owner"])
|
|
763
|
+
|
|
764
|
+
def _add_created_metadata_to_query(self) -> None:
|
|
765
|
+
if not self._include_created_metadata:
|
|
766
|
+
return
|
|
767
|
+
if self.branch.is_default or self.branch.is_global:
|
|
768
|
+
last_created_query = """
|
|
769
|
+
WITH *, a.created_at AS created_at, a.created_by AS created_by
|
|
770
|
+
"""
|
|
771
|
+
else:
|
|
772
|
+
last_created_query = """
|
|
773
|
+
CALL (a) {
|
|
774
|
+
MATCH ()-[e:HAS_ATTRIBUTE {status: "active"}]->(a)
|
|
775
|
+
RETURN e.from AS created_at, e.from_user_id AS created_by
|
|
776
|
+
ORDER BY e.from ASC
|
|
777
|
+
LIMIT 1
|
|
778
|
+
}
|
|
779
|
+
"""
|
|
780
|
+
self.add_to_query(last_created_query)
|
|
781
|
+
self.return_labels.extend(["created_at", "created_by"])
|
|
782
|
+
|
|
783
|
+
def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
784
|
+
if not self._include_updated_metadata:
|
|
785
|
+
return
|
|
786
|
+
if self.branch.is_default or self.branch.is_global:
|
|
787
|
+
last_updated_query = """
|
|
788
|
+
WITH *, a.updated_at AS updated_at, a.updated_by AS updated_by
|
|
789
|
+
"""
|
|
790
|
+
else:
|
|
791
|
+
if self.branch_agnostic:
|
|
792
|
+
time_details = """
|
|
793
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
794
|
+
"""
|
|
795
|
+
else:
|
|
796
|
+
time_details = """
|
|
797
|
+
WITH CASE
|
|
798
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
799
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
800
|
+
ELSE [NULL, NULL]
|
|
801
|
+
END AS from_details,
|
|
802
|
+
CASE
|
|
803
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
804
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
805
|
+
ELSE [NULL, NULL]
|
|
806
|
+
END AS to_details
|
|
807
|
+
"""
|
|
808
|
+
last_updated_query = """
|
|
809
|
+
CALL (a) {
|
|
810
|
+
MATCH (a)-[r]->(property)
|
|
811
|
+
WHERE %(branch_filter)s
|
|
812
|
+
%(time_details)s
|
|
813
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
814
|
+
WITH from_details_list + to_details_list AS details_list
|
|
815
|
+
UNWIND details_list AS one_details
|
|
816
|
+
WITH one_details[0] AS updated_at, one_details[1] AS updated_by
|
|
817
|
+
WHERE updated_at IS NOT NULL
|
|
818
|
+
WITH updated_at, updated_by
|
|
819
|
+
ORDER BY updated_at DESC
|
|
820
|
+
LIMIT 1
|
|
821
|
+
RETURN updated_at, updated_by
|
|
822
|
+
}
|
|
823
|
+
""" % {"branch_filter": branch_filter_str, "time_details": time_details}
|
|
824
|
+
self.add_to_query(last_updated_query)
|
|
825
|
+
self.return_labels.extend(["updated_at", "updated_by"])
|
|
826
|
+
|
|
626
827
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
627
828
|
self.params["ids"] = self.ids
|
|
628
829
|
self.params["profile_node_relationship_name"] = PROFILE_NODE_RELATIONSHIP_IDENTIFIER
|
|
629
830
|
self.params["profile_template_relationship_name"] = PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER
|
|
831
|
+
self.params["field_names"] = list(self.fields.keys()) if self.fields else []
|
|
630
832
|
|
|
631
833
|
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
632
834
|
at=self.at, branch_agnostic=self.branch_agnostic
|
|
@@ -640,11 +842,9 @@ class NodeListGetAttributeQuery(Query):
|
|
|
640
842
|
exists((n)-[:IS_RELATED]-(:Relationship {name: $profile_template_relationship_name}))
|
|
641
843
|
) AS might_use_profile
|
|
642
844
|
MATCH (n)-[:HAS_ATTRIBUTE]-(a:Attribute)
|
|
845
|
+
WHERE (a.name IN $field_names OR size($field_names) = 0)
|
|
846
|
+
WITH DISTINCT n, a, might_use_profile
|
|
643
847
|
"""
|
|
644
|
-
if self.fields:
|
|
645
|
-
query += "\n WHERE a.name IN $field_names"
|
|
646
|
-
self.params["field_names"] = list(self.fields.keys())
|
|
647
|
-
|
|
648
848
|
self.add_to_query(query)
|
|
649
849
|
|
|
650
850
|
query = """
|
|
@@ -680,15 +880,8 @@ WHERE r2.status = "active"
|
|
|
680
880
|
|
|
681
881
|
self.return_labels = ["n", "a", "av", "r1", "r2", "is_from_profile"]
|
|
682
882
|
|
|
683
|
-
# Add Is_Protected
|
|
883
|
+
# Add Is_Protected
|
|
684
884
|
query = """
|
|
685
|
-
CALL (a) {
|
|
686
|
-
MATCH (a)-[r:IS_VISIBLE]-(isv:Boolean)
|
|
687
|
-
WHERE (%(branch_filter)s)
|
|
688
|
-
RETURN r AS rel_isv, isv
|
|
689
|
-
ORDER BY rel_isv.branch_level DESC, rel_isv.from DESC, rel_isv.status ASC
|
|
690
|
-
LIMIT 1
|
|
691
|
-
}
|
|
692
885
|
CALL (a) {
|
|
693
886
|
MATCH (a)-[r:IS_PROTECTED]-(isp:Boolean)
|
|
694
887
|
WHERE (%(branch_filter)s)
|
|
@@ -699,39 +892,12 @@ CALL (a) {
|
|
|
699
892
|
""" % {"branch_filter": branch_filter}
|
|
700
893
|
self.add_to_query(query)
|
|
701
894
|
|
|
702
|
-
self.return_labels.extend(["
|
|
895
|
+
self.return_labels.extend(["isp", "rel_isp"])
|
|
703
896
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
WHERE all(r IN [rel_source] WHERE ( %(branch_filter)s ))
|
|
709
|
-
RETURN source, rel_source
|
|
710
|
-
ORDER BY rel_source.branch_level DESC, rel_source.from DESC, rel_source.status ASC
|
|
711
|
-
LIMIT 1
|
|
712
|
-
}
|
|
713
|
-
WITH *,
|
|
714
|
-
CASE WHEN rel_source.status = "active" THEN source ELSE NULL END AS source,
|
|
715
|
-
CASE WHEN rel_source.status = "active" THEN rel_source ELSE NULL END AS rel_source
|
|
716
|
-
""" % {"branch_filter": branch_filter}
|
|
717
|
-
self.add_to_query(query)
|
|
718
|
-
self.return_labels.extend(["source", "rel_source"])
|
|
719
|
-
|
|
720
|
-
if self.include_owner:
|
|
721
|
-
query = """
|
|
722
|
-
CALL (a) {
|
|
723
|
-
OPTIONAL MATCH (a)-[rel_owner:HAS_OWNER]-(owner)
|
|
724
|
-
WHERE all(r IN [rel_owner] WHERE ( %(branch_filter)s ))
|
|
725
|
-
RETURN owner, rel_owner
|
|
726
|
-
ORDER BY rel_owner.branch_level DESC, rel_owner.from DESC, rel_owner.status ASC
|
|
727
|
-
LIMIT 1
|
|
728
|
-
}
|
|
729
|
-
WITH *,
|
|
730
|
-
CASE WHEN rel_owner.status = "active" THEN owner ELSE NULL END AS owner,
|
|
731
|
-
CASE WHEN rel_owner.status = "active" THEN rel_owner ELSE NULL END AS rel_owner
|
|
732
|
-
""" % {"branch_filter": branch_filter}
|
|
733
|
-
self.add_to_query(query)
|
|
734
|
-
self.return_labels.extend(["owner", "rel_owner"])
|
|
897
|
+
self._add_source_to_query(branch_filter_str=branch_filter)
|
|
898
|
+
self._add_owner_to_query(branch_filter_str=branch_filter)
|
|
899
|
+
self._add_created_metadata_to_query()
|
|
900
|
+
self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
|
|
735
901
|
|
|
736
902
|
def get_attributes_group_by_node(self) -> dict[str, NodeAttributesFromDB]:
|
|
737
903
|
attrs_by_node: dict[str, NodeAttributesFromDB] = {}
|
|
@@ -768,7 +934,6 @@ CALL (a) {
|
|
|
768
934
|
attr_uuid=attr.get("uuid"),
|
|
769
935
|
attr_value_id=attr_value.element_id,
|
|
770
936
|
attr_value_uuid=attr_value.get("uuid"),
|
|
771
|
-
updated_at=result.get_rel("r2").get("from"),
|
|
772
937
|
value=attr_value.get("value"),
|
|
773
938
|
is_default=attr_value.get("is_default"),
|
|
774
939
|
is_from_profile=is_from_profile,
|
|
@@ -776,16 +941,26 @@ CALL (a) {
|
|
|
776
941
|
branch=self.branch.name,
|
|
777
942
|
flag_properties={
|
|
778
943
|
"is_protected": result.get("isp").get("value"),
|
|
779
|
-
"is_visible": result.get("isv").get("value"),
|
|
780
944
|
},
|
|
781
945
|
)
|
|
782
946
|
|
|
783
|
-
if self.
|
|
947
|
+
if self.include_metadata & MetadataOptions.CREATED_AT:
|
|
948
|
+
created_at_str = result.get_as_str("created_at")
|
|
949
|
+
data.created_at = Timestamp(created_at_str) if created_at_str else None
|
|
950
|
+
if self.include_metadata & MetadataOptions.CREATED_BY:
|
|
951
|
+
data.created_by = result.get_as_str("created_by")
|
|
952
|
+
if self.include_metadata & MetadataOptions.UPDATED_AT:
|
|
953
|
+
updated_at_str = result.get_as_str("updated_at")
|
|
954
|
+
data.updated_at = Timestamp(updated_at_str) if updated_at_str else None
|
|
955
|
+
if self.include_metadata & MetadataOptions.UPDATED_BY:
|
|
956
|
+
data.updated_by = result.get_as_str("updated_by")
|
|
957
|
+
|
|
958
|
+
if self._include_source and result.get("source"):
|
|
784
959
|
data.node_properties["source"] = AttributeNodePropertyFromDB(
|
|
785
960
|
uuid=result.get_node("source").get("uuid"), labels=list(result.get_node("source").labels)
|
|
786
961
|
)
|
|
787
962
|
|
|
788
|
-
if self.
|
|
963
|
+
if self._include_owner and result.get("owner"):
|
|
789
964
|
data.node_properties["owner"] = AttributeNodePropertyFromDB(
|
|
790
965
|
uuid=result.get_node("owner").get("uuid"), labels=list(result.get_node("owner").labels)
|
|
791
966
|
)
|
|
@@ -794,17 +969,42 @@ CALL (a) {
|
|
|
794
969
|
|
|
795
970
|
|
|
796
971
|
class GroupedPeerNodes:
|
|
797
|
-
def __init__(self):
|
|
972
|
+
def __init__(self) -> None:
|
|
798
973
|
# {node_id: [rel_name, ...]}
|
|
799
974
|
self._rel_names_by_node_id: dict[str, set[str]] = defaultdict(set)
|
|
800
975
|
# {(node_id, rel_name): {RelationshipDirection: {peer_id, ...}}}
|
|
801
976
|
self._rel_directions_map: dict[tuple[str, str], dict[RelationshipDirection, set[str]]] = defaultdict(dict)
|
|
977
|
+
# {(node_id, rel_name, direction, peer_Id): {MetadataOptions: value}}
|
|
978
|
+
self._metadata_map: dict[
|
|
979
|
+
tuple[str, str, RelationshipDirection, str], dict[MetadataOptions, Timestamp | str | None]
|
|
980
|
+
] = {}
|
|
802
981
|
|
|
803
|
-
def add_peer(
|
|
982
|
+
def add_peer(
|
|
983
|
+
self,
|
|
984
|
+
node_id: str,
|
|
985
|
+
rel_name: str,
|
|
986
|
+
peer_id: str,
|
|
987
|
+
direction: RelationshipDirection,
|
|
988
|
+
created_at: Timestamp | None = None,
|
|
989
|
+
created_by: str | None = None,
|
|
990
|
+
updated_at: Timestamp | None = None,
|
|
991
|
+
updated_by: str | None = None,
|
|
992
|
+
) -> None:
|
|
804
993
|
self._rel_names_by_node_id[node_id].add(rel_name)
|
|
805
994
|
if direction not in self._rel_directions_map[node_id, rel_name]:
|
|
806
995
|
self._rel_directions_map[node_id, rel_name][direction] = set()
|
|
807
996
|
self._rel_directions_map[node_id, rel_name][direction].add(peer_id)
|
|
997
|
+
key = (node_id, rel_name, direction, peer_id)
|
|
998
|
+
if created_at is not None or created_by is not None or updated_at is not None or updated_by is not None:
|
|
999
|
+
self._metadata_map[key] = {}
|
|
1000
|
+
if created_at is not None:
|
|
1001
|
+
self._metadata_map[key][MetadataOptions.CREATED_AT] = created_at
|
|
1002
|
+
if created_by is not None:
|
|
1003
|
+
self._metadata_map[key][MetadataOptions.CREATED_BY] = created_by
|
|
1004
|
+
if updated_at is not None:
|
|
1005
|
+
self._metadata_map[key][MetadataOptions.UPDATED_AT] = updated_at
|
|
1006
|
+
if updated_by is not None:
|
|
1007
|
+
self._metadata_map[key][MetadataOptions.UPDATED_BY] = updated_by
|
|
808
1008
|
|
|
809
1009
|
def get_peer_ids(self, node_id: str, rel_name: str, direction: RelationshipDirection) -> set[str]:
|
|
810
1010
|
if (node_id, rel_name) not in self._rel_directions_map:
|
|
@@ -821,11 +1021,15 @@ class GroupedPeerNodes:
|
|
|
821
1021
|
def has_node(self, node_id: str) -> bool:
|
|
822
1022
|
return node_id in self._rel_names_by_node_id
|
|
823
1023
|
|
|
1024
|
+
def get_metadata_map(
|
|
1025
|
+
self, node_id: str, rel_name: str, direction: RelationshipDirection, peer_id: str
|
|
1026
|
+
) -> dict[MetadataOptions, Timestamp | str | None]:
|
|
1027
|
+
return self._metadata_map.get((node_id, rel_name, direction, peer_id), {})
|
|
1028
|
+
|
|
824
1029
|
|
|
825
1030
|
class NodeListGetRelationshipsQuery(Query):
|
|
826
1031
|
name: str = "node_list_get_relationship"
|
|
827
1032
|
type: QueryType = QueryType.READ
|
|
828
|
-
insert_return: bool = False
|
|
829
1033
|
|
|
830
1034
|
def __init__(
|
|
831
1035
|
self,
|
|
@@ -833,14 +1037,80 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
833
1037
|
outbound_identifiers: list[str] | None = None,
|
|
834
1038
|
inbound_identifiers: list[str] | None = None,
|
|
835
1039
|
bidirectional_identifiers: list[str] | None = None,
|
|
1040
|
+
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
836
1041
|
**kwargs,
|
|
837
|
-
):
|
|
1042
|
+
) -> None:
|
|
838
1043
|
self.ids = ids
|
|
839
1044
|
self.outbound_identifiers = outbound_identifiers
|
|
840
1045
|
self.inbound_identifiers = inbound_identifiers
|
|
841
1046
|
self.bidirectional_identifiers = bidirectional_identifiers
|
|
1047
|
+
self.include_metadata = include_metadata
|
|
842
1048
|
super().__init__(**kwargs)
|
|
843
1049
|
|
|
1050
|
+
def _add_created_metadata_to_query(self) -> None:
|
|
1051
|
+
if not (self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY)):
|
|
1052
|
+
return
|
|
1053
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1054
|
+
last_created_query = """
|
|
1055
|
+
WITH *, rel.created_at AS created_at, rel.created_by AS created_by
|
|
1056
|
+
"""
|
|
1057
|
+
else:
|
|
1058
|
+
last_created_query = """
|
|
1059
|
+
WITH *, CASE
|
|
1060
|
+
WHEN r1.from < r2.from THEN [r1.from, r1.from_user_id]
|
|
1061
|
+
ELSE [r2.from, r2.from_user_id]
|
|
1062
|
+
END AS created_details
|
|
1063
|
+
WITH *, created_details[0] AS created_at, created_details[1] AS created_by
|
|
1064
|
+
"""
|
|
1065
|
+
self.add_to_query(last_created_query)
|
|
1066
|
+
self.return_labels.extend(["created_at", "created_by"])
|
|
1067
|
+
|
|
1068
|
+
def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
1069
|
+
if not (self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY)):
|
|
1070
|
+
return
|
|
1071
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1072
|
+
last_updated_query = """
|
|
1073
|
+
WITH *, rel.updated_at AS updated_at, rel.updated_by AS updated_by
|
|
1074
|
+
"""
|
|
1075
|
+
else:
|
|
1076
|
+
if self.branch_agnostic:
|
|
1077
|
+
time_details = """
|
|
1078
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
1079
|
+
"""
|
|
1080
|
+
else:
|
|
1081
|
+
time_details = """
|
|
1082
|
+
WITH CASE
|
|
1083
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
1084
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
1085
|
+
ELSE [NULL, NULL]
|
|
1086
|
+
END AS from_details,
|
|
1087
|
+
CASE
|
|
1088
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
1089
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
1090
|
+
ELSE [NULL, NULL]
|
|
1091
|
+
END AS to_details
|
|
1092
|
+
"""
|
|
1093
|
+
last_updated_query = """
|
|
1094
|
+
CALL (rel) {
|
|
1095
|
+
// don't use IS_RELATED edges to handle the case when at least one of the
|
|
1096
|
+
// peers is a migrated-kind node
|
|
1097
|
+
MATCH (rel)-[r:!IS_RELATED]->(property)
|
|
1098
|
+
WHERE %(branch_filter)s
|
|
1099
|
+
%(time_details)s
|
|
1100
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
1101
|
+
WITH from_details_list + to_details_list AS details_list
|
|
1102
|
+
UNWIND details_list AS one_details
|
|
1103
|
+
WITH one_details[0] AS updated_at, one_details[1] AS updated_by
|
|
1104
|
+
WHERE updated_at IS NOT NULL
|
|
1105
|
+
WITH updated_at, updated_by
|
|
1106
|
+
ORDER BY updated_at DESC
|
|
1107
|
+
LIMIT 1
|
|
1108
|
+
RETURN updated_at, updated_by
|
|
1109
|
+
}
|
|
1110
|
+
""" % {"branch_filter": branch_filter_str, "time_details": time_details}
|
|
1111
|
+
self.add_to_query(last_updated_query)
|
|
1112
|
+
self.return_labels.extend(["updated_at", "updated_by"])
|
|
1113
|
+
|
|
844
1114
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
845
1115
|
self.params["ids"] = self.ids
|
|
846
1116
|
self.params["outbound_identifiers"] = self.outbound_identifiers
|
|
@@ -872,9 +1142,9 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
872
1142
|
LIMIT 1
|
|
873
1143
|
WITH r1, r AS r2
|
|
874
1144
|
WHERE r2.status = "active"
|
|
875
|
-
RETURN
|
|
1145
|
+
RETURN r1, r2
|
|
876
1146
|
}
|
|
877
|
-
RETURN n.uuid AS n_uuid, rel
|
|
1147
|
+
RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "inbound" as direction, r1, r2
|
|
878
1148
|
UNION
|
|
879
1149
|
WITH n
|
|
880
1150
|
MATCH (n)-[:IS_RELATED]->(rel:Relationship)-[:IS_RELATED]->(peer)
|
|
@@ -896,9 +1166,9 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
896
1166
|
LIMIT 1
|
|
897
1167
|
WITH r1, r AS r2
|
|
898
1168
|
WHERE r2.status = "active"
|
|
899
|
-
RETURN
|
|
1169
|
+
RETURN r1, r2
|
|
900
1170
|
}
|
|
901
|
-
RETURN n.uuid AS n_uuid, rel
|
|
1171
|
+
RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "outbound" as direction, r1, r2
|
|
902
1172
|
UNION
|
|
903
1173
|
WITH n
|
|
904
1174
|
MATCH (n)-[:IS_RELATED]->(rel:Relationship)<-[:IS_RELATED]-(peer)
|
|
@@ -920,15 +1190,21 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
920
1190
|
LIMIT 1
|
|
921
1191
|
WITH r1, r AS r2
|
|
922
1192
|
WHERE r2.status = "active"
|
|
923
|
-
RETURN
|
|
1193
|
+
RETURN r1, r2
|
|
924
1194
|
}
|
|
925
|
-
RETURN n.uuid AS n_uuid, rel
|
|
1195
|
+
RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "bidirectional" as direction, r1, r2
|
|
926
1196
|
}
|
|
927
|
-
RETURN DISTINCT n_uuid, rel_name, peer_uuid, direction
|
|
928
1197
|
""" % {"filters": rels_filter}
|
|
929
1198
|
self.add_to_query(query)
|
|
1199
|
+
|
|
930
1200
|
self.order_by = ["n_uuid", "rel_name", "peer_uuid", "direction"]
|
|
931
|
-
self.return_labels = ["n_uuid", "
|
|
1201
|
+
self.return_labels = ["n_uuid", "peer_uuid", "direction"]
|
|
1202
|
+
|
|
1203
|
+
self._add_created_metadata_to_query()
|
|
1204
|
+
self._add_updated_metadata_to_query(branch_filter_str=rels_filter)
|
|
1205
|
+
return_labels_str = ", ".join(sorted(self.return_labels))
|
|
1206
|
+
self.add_to_query(f"WITH DISTINCT {return_labels_str}, rel.name AS rel_name")
|
|
1207
|
+
self.return_labels.append("rel_name")
|
|
932
1208
|
|
|
933
1209
|
def get_peers_group_by_node(self) -> GroupedPeerNodes:
|
|
934
1210
|
gpn = GroupedPeerNodes()
|
|
@@ -937,12 +1213,40 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
937
1213
|
rel_name = result.get("rel_name")
|
|
938
1214
|
peer_id = result.get("peer_uuid")
|
|
939
1215
|
direction = str(result.get("direction"))
|
|
1216
|
+
|
|
1217
|
+
created_at = None
|
|
1218
|
+
if self.include_metadata & MetadataOptions.CREATED_AT:
|
|
1219
|
+
created_at_str = result.get("created_at")
|
|
1220
|
+
created_at = Timestamp(created_at_str) if created_at_str else None
|
|
1221
|
+
|
|
1222
|
+
created_by_str = None
|
|
1223
|
+
if self.include_metadata & MetadataOptions.CREATED_BY:
|
|
1224
|
+
created_by_str = result.get("created_by")
|
|
1225
|
+
|
|
1226
|
+
updated_at = None
|
|
1227
|
+
if self.include_metadata & MetadataOptions.UPDATED_AT:
|
|
1228
|
+
updated_at_str = result.get("updated_at")
|
|
1229
|
+
updated_at = Timestamp(updated_at_str) if updated_at_str else None
|
|
1230
|
+
|
|
1231
|
+
updated_by_str = None
|
|
1232
|
+
if self.include_metadata & MetadataOptions.UPDATED_BY:
|
|
1233
|
+
updated_by_str = result.get("updated_by")
|
|
1234
|
+
|
|
940
1235
|
direction_enum = {
|
|
941
1236
|
"inbound": RelationshipDirection.INBOUND,
|
|
942
1237
|
"outbound": RelationshipDirection.OUTBOUND,
|
|
943
1238
|
"bidirectional": RelationshipDirection.BIDIR,
|
|
944
1239
|
}.get(direction)
|
|
945
|
-
gpn.add_peer(
|
|
1240
|
+
gpn.add_peer(
|
|
1241
|
+
node_id=node_id,
|
|
1242
|
+
rel_name=rel_name,
|
|
1243
|
+
peer_id=peer_id,
|
|
1244
|
+
direction=direction_enum,
|
|
1245
|
+
created_at=created_at,
|
|
1246
|
+
created_by=created_by_str,
|
|
1247
|
+
updated_at=updated_at,
|
|
1248
|
+
updated_by=updated_by_str,
|
|
1249
|
+
)
|
|
946
1250
|
|
|
947
1251
|
return gpn
|
|
948
1252
|
|
|
@@ -986,11 +1290,81 @@ class NodeListGetInfoQuery(Query):
|
|
|
986
1290
|
name = "node_list_get_info"
|
|
987
1291
|
type = QueryType.READ
|
|
988
1292
|
|
|
989
|
-
def __init__(self, ids: list[str],
|
|
990
|
-
self.account = account
|
|
1293
|
+
def __init__(self, ids: list[str], include_metadata: MetadataOptions = MetadataOptions.NONE, **kwargs: Any) -> None:
|
|
991
1294
|
self.ids = ids
|
|
1295
|
+
self.include_metadata = include_metadata
|
|
992
1296
|
super().__init__(**kwargs)
|
|
993
1297
|
|
|
1298
|
+
def _needs_user_timestamp_metadata(self) -> bool:
|
|
1299
|
+
return bool(self.include_metadata & MetadataOptions.USER_TIMESTAMPS)
|
|
1300
|
+
|
|
1301
|
+
def _add_created_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
1302
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1303
|
+
created_metadata_query = """
|
|
1304
|
+
WITH *, n.created_at AS created_at, n.created_by AS created_by
|
|
1305
|
+
"""
|
|
1306
|
+
else:
|
|
1307
|
+
created_metadata_query = """
|
|
1308
|
+
CALL (n) {
|
|
1309
|
+
MATCH (:Node {uuid: n.uuid})-[r:IS_PART_OF {status: "active"}]->(:Root)
|
|
1310
|
+
WHERE %(branch_filter)s
|
|
1311
|
+
RETURN r.from AS created_at, r.from_user_id AS created_by
|
|
1312
|
+
ORDER BY r.from ASC
|
|
1313
|
+
LIMIT 1
|
|
1314
|
+
}
|
|
1315
|
+
""" % {"branch_filter": branch_filter_str}
|
|
1316
|
+
self.add_to_query(created_metadata_query)
|
|
1317
|
+
self.return_labels.extend(["created_at", "created_by"])
|
|
1318
|
+
|
|
1319
|
+
def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
1320
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1321
|
+
last_update_query = """
|
|
1322
|
+
WITH *, n.updated_at AS updated_at, n.updated_by AS updated_by
|
|
1323
|
+
"""
|
|
1324
|
+
else:
|
|
1325
|
+
if self.branch_agnostic:
|
|
1326
|
+
time_details = """
|
|
1327
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
1328
|
+
"""
|
|
1329
|
+
else:
|
|
1330
|
+
time_details = """
|
|
1331
|
+
WITH CASE
|
|
1332
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
1333
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
1334
|
+
ELSE [NULL, NULL]
|
|
1335
|
+
END AS from_details,
|
|
1336
|
+
CASE
|
|
1337
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
1338
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
1339
|
+
ELSE [NULL, NULL]
|
|
1340
|
+
END AS to_details
|
|
1341
|
+
"""
|
|
1342
|
+
last_update_query = """
|
|
1343
|
+
MATCH (n)-[r:HAS_ATTRIBUTE|IS_RELATED]-(field:Attribute|Relationship)
|
|
1344
|
+
WHERE %(branch_filter)s
|
|
1345
|
+
WITH DISTINCT n, r_is_part_of, field
|
|
1346
|
+
CALL (field) {
|
|
1347
|
+
MATCH (field)-[r]-(property)
|
|
1348
|
+
WHERE %(branch_filter)s
|
|
1349
|
+
%(time_details)s
|
|
1350
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
1351
|
+
WITH from_details_list + to_details_list AS details_list
|
|
1352
|
+
UNWIND details_list AS one_details
|
|
1353
|
+
WITH one_details[0] AS updated_at, one_details[1] AS updated_by
|
|
1354
|
+
WHERE updated_at IS NOT NULL
|
|
1355
|
+
WITH updated_at, updated_by
|
|
1356
|
+
ORDER BY updated_at DESC
|
|
1357
|
+
LIMIT 1
|
|
1358
|
+
RETURN updated_at, updated_by
|
|
1359
|
+
}
|
|
1360
|
+
WITH n, r_is_part_of, updated_at, updated_by
|
|
1361
|
+
// updated_by ordering preferences non "__system__" users
|
|
1362
|
+
ORDER BY elementId(n), updated_at DESC, updated_by DESC
|
|
1363
|
+
WITH n, r_is_part_of, head(collect(updated_at)) AS updated_at, head(collect(updated_by)) AS updated_by
|
|
1364
|
+
""" % {"branch_filter": branch_filter_str, "time_details": time_details}
|
|
1365
|
+
self.add_to_query(last_update_query)
|
|
1366
|
+
self.return_labels.extend(["updated_at", "updated_by"])
|
|
1367
|
+
|
|
994
1368
|
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
995
1369
|
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
996
1370
|
at=self.at, branch_agnostic=self.branch_agnostic
|
|
@@ -1005,36 +1379,115 @@ class NodeListGetInfoQuery(Query):
|
|
|
1005
1379
|
CALL (root, n) {
|
|
1006
1380
|
MATCH (root:Root)<-[r:IS_PART_OF]-(n:Node)
|
|
1007
1381
|
WHERE %(branch_filter)s
|
|
1008
|
-
RETURN
|
|
1009
|
-
ORDER BY r.branch_level DESC, r.from DESC
|
|
1382
|
+
RETURN r AS r_is_part_of
|
|
1383
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
1010
1384
|
LIMIT 1
|
|
1011
1385
|
}
|
|
1012
|
-
WITH
|
|
1013
|
-
WHERE
|
|
1386
|
+
WITH n, r_is_part_of
|
|
1387
|
+
WHERE r_is_part_of.status = "active"
|
|
1014
1388
|
""" % {"branch_filter": branch_filter}
|
|
1015
|
-
|
|
1016
1389
|
self.add_to_query(query)
|
|
1390
|
+
self.return_labels = [
|
|
1391
|
+
"labels(n) AS node_labels",
|
|
1392
|
+
"r_is_part_of.branch AS branch",
|
|
1393
|
+
"elementId(n) AS node_database_id",
|
|
1394
|
+
"n.uuid AS node_uuid",
|
|
1395
|
+
]
|
|
1017
1396
|
|
|
1018
|
-
self.
|
|
1397
|
+
if self._needs_user_timestamp_metadata():
|
|
1398
|
+
self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
|
|
1399
|
+
self._add_created_metadata_to_query(branch_filter_str=branch_filter)
|
|
1019
1400
|
|
|
1020
1401
|
async def get_nodes(self, db: InfrahubDatabase, duplicate: bool = False) -> AsyncIterator[NodeToProcess]:
|
|
1021
1402
|
"""Return all the node objects as NodeToProcess."""
|
|
1022
1403
|
|
|
1023
|
-
for result in self.
|
|
1024
|
-
|
|
1404
|
+
for result in self.get_results():
|
|
1405
|
+
raw_labels: list[str] = result.get_as_type(label="node_labels", return_type=list)
|
|
1406
|
+
labels = [str(lbl) for lbl in raw_labels]
|
|
1407
|
+
schema = find_node_schema(db=db, branch=self.branch, labels=labels, duplicate=duplicate)
|
|
1025
1408
|
node_branch = self.branch
|
|
1026
1409
|
if self.branch_agnostic:
|
|
1027
|
-
node_branch = result.
|
|
1410
|
+
node_branch = result.get_as_type(label="branch", return_type=str)
|
|
1411
|
+
|
|
1412
|
+
created_at = None
|
|
1413
|
+
created_by = None
|
|
1414
|
+
if self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.UPDATED_AT):
|
|
1415
|
+
raw_created_at = result.get_as_str(label="created_at")
|
|
1416
|
+
created_at = Timestamp(raw_created_at) if raw_created_at else None
|
|
1417
|
+
if self.include_metadata & (MetadataOptions.CREATED_BY | MetadataOptions.UPDATED_BY):
|
|
1418
|
+
created_by = result.get_as_str(label="created_by")
|
|
1419
|
+
updated_at = None
|
|
1420
|
+
updated_by = None
|
|
1421
|
+
if self.include_metadata & MetadataOptions.UPDATED_AT:
|
|
1422
|
+
raw_updated_at = result.get_as_str(label="updated_at")
|
|
1423
|
+
updated_at = Timestamp(raw_updated_at) if raw_updated_at else None
|
|
1424
|
+
if self.include_metadata & MetadataOptions.UPDATED_BY:
|
|
1425
|
+
updated_by = result.get_as_str(label="updated_by")
|
|
1426
|
+
|
|
1028
1427
|
yield NodeToProcess(
|
|
1029
1428
|
schema=schema,
|
|
1030
|
-
node_id=result.
|
|
1031
|
-
node_uuid=result.
|
|
1032
|
-
updated_at=result.get_rel("rb").get("from"),
|
|
1429
|
+
node_id=result.get_as_type(label="node_database_id", return_type=str),
|
|
1430
|
+
node_uuid=result.get_as_type(label="node_uuid", return_type=str),
|
|
1033
1431
|
branch=node_branch,
|
|
1034
|
-
labels=
|
|
1432
|
+
labels=labels,
|
|
1433
|
+
created_at=created_at,
|
|
1434
|
+
created_by=created_by,
|
|
1435
|
+
updated_at=updated_at or created_at,
|
|
1436
|
+
updated_by=updated_by or created_by,
|
|
1035
1437
|
)
|
|
1036
1438
|
|
|
1037
1439
|
|
|
1440
|
+
class NodeGetByHFIDQuery(Query):
|
|
1441
|
+
"""Query to lookup nodes by their HFID.
|
|
1442
|
+
|
|
1443
|
+
This query uses the stored `human_friendly_id` attribute on nodes.
|
|
1444
|
+
"""
|
|
1445
|
+
|
|
1446
|
+
name = "node_get_by_hfid"
|
|
1447
|
+
type = QueryType.READ
|
|
1448
|
+
|
|
1449
|
+
def __init__(self, node_kind: str, hfids: list[list[str]], **kwargs: Any) -> None:
|
|
1450
|
+
self.node_kind = node_kind
|
|
1451
|
+
self.hfids = hfids
|
|
1452
|
+
super().__init__(**kwargs)
|
|
1453
|
+
|
|
1454
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
1455
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
|
|
1456
|
+
self.params.update(branch_params)
|
|
1457
|
+
# The list is stored as a string in the database
|
|
1458
|
+
self.params["hfid_values"] = [ujson.dumps(hfid) for hfid in self.hfids]
|
|
1459
|
+
|
|
1460
|
+
query = """
|
|
1461
|
+
MATCH (n:%(node_kind)s)
|
|
1462
|
+
CALL (n) {
|
|
1463
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
1464
|
+
WHERE %(branch_filter)s
|
|
1465
|
+
RETURN r AS r_part_of
|
|
1466
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
1467
|
+
LIMIT 1
|
|
1468
|
+
}
|
|
1469
|
+
WITH n, r_part_of
|
|
1470
|
+
WHERE r_part_of.status = "active"
|
|
1471
|
+
MATCH (n)-[:HAS_ATTRIBUTE]->(attr:Attribute {name: "human_friendly_id"})
|
|
1472
|
+
CALL (attr) {
|
|
1473
|
+
MATCH (attr)-[r:HAS_VALUE]->(av)
|
|
1474
|
+
WHERE %(branch_filter)s
|
|
1475
|
+
RETURN av, r AS r_attr
|
|
1476
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
1477
|
+
LIMIT 1
|
|
1478
|
+
}
|
|
1479
|
+
WITH n, av, r_attr
|
|
1480
|
+
WHERE r_attr.status = "active" AND av.value IN $hfid_values
|
|
1481
|
+
""" % {"branch_filter": branch_filter, "node_kind": self.node_kind}
|
|
1482
|
+
|
|
1483
|
+
self.add_to_query(query)
|
|
1484
|
+
self.return_labels = ["n.uuid AS node_uuid", "av.value AS hfid"]
|
|
1485
|
+
|
|
1486
|
+
def get_node_uuids(self) -> list[str]:
|
|
1487
|
+
"""Get the list of node UUIDs from the query results."""
|
|
1488
|
+
return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
|
|
1489
|
+
|
|
1490
|
+
|
|
1038
1491
|
class FieldAttributeRequirementType(Enum):
|
|
1039
1492
|
FILTER = "filter"
|
|
1040
1493
|
ORDER = "order"
|
|
@@ -1048,6 +1501,9 @@ class FieldAttributeRequirement:
|
|
|
1048
1501
|
field_attr_value: Any
|
|
1049
1502
|
index: int
|
|
1050
1503
|
types: list[FieldAttributeRequirementType] = dataclass_field(default_factory=list)
|
|
1504
|
+
order_direction: OrderDirection | None = None
|
|
1505
|
+
# created_at, updated_at, created_by, updated_by
|
|
1506
|
+
is_metadata: bool = False
|
|
1051
1507
|
|
|
1052
1508
|
@property
|
|
1053
1509
|
def is_attribute_value(self) -> bool:
|
|
@@ -1061,6 +1517,14 @@ class FieldAttributeRequirement:
|
|
|
1061
1517
|
def is_order(self) -> bool:
|
|
1062
1518
|
return FieldAttributeRequirementType.ORDER in self.types
|
|
1063
1519
|
|
|
1520
|
+
@property
|
|
1521
|
+
def is_metadata_order(self) -> bool:
|
|
1522
|
+
return self.is_metadata and FieldAttributeRequirementType.ORDER in self.types
|
|
1523
|
+
|
|
1524
|
+
@property
|
|
1525
|
+
def is_metadata_filter(self) -> bool:
|
|
1526
|
+
return self.is_metadata and FieldAttributeRequirementType.FILTER in self.types
|
|
1527
|
+
|
|
1064
1528
|
@property
|
|
1065
1529
|
def node_value_query_variable(self) -> str:
|
|
1066
1530
|
return f"attr{self.index}_node_value"
|
|
@@ -1069,8 +1533,12 @@ class FieldAttributeRequirement:
|
|
|
1069
1533
|
def comparison_operator(self) -> str:
|
|
1070
1534
|
if self.field_attr_name == "isnull":
|
|
1071
1535
|
return "=" if self.field_attr_value is True else "<>"
|
|
1072
|
-
if self.field_attr_name
|
|
1536
|
+
if self.field_attr_name in ("values", "ids"):
|
|
1073
1537
|
return "IN"
|
|
1538
|
+
if self.field_attr_name == "before":
|
|
1539
|
+
return "<"
|
|
1540
|
+
if self.field_attr_name == "after":
|
|
1541
|
+
return ">"
|
|
1074
1542
|
return "="
|
|
1075
1543
|
|
|
1076
1544
|
@property
|
|
@@ -1106,7 +1574,7 @@ class NodeGetListQuery(Query):
|
|
|
1106
1574
|
order = copy(order)
|
|
1107
1575
|
order.disable = True
|
|
1108
1576
|
|
|
1109
|
-
self.
|
|
1577
|
+
self.requested_order = order
|
|
1110
1578
|
|
|
1111
1579
|
super().__init__(**kwargs)
|
|
1112
1580
|
|
|
@@ -1122,6 +1590,25 @@ class NodeGetListQuery(Query):
|
|
|
1122
1590
|
return True
|
|
1123
1591
|
return False
|
|
1124
1592
|
|
|
1593
|
+
def _get_metadata_order_fields(self) -> list[tuple[str, OrderDirection]]:
|
|
1594
|
+
"""Return the metadata field and direction to order by, or None."""
|
|
1595
|
+
if not self.requested_order or not self.requested_order.node_metadata:
|
|
1596
|
+
return []
|
|
1597
|
+
fields: list[tuple[str, OrderDirection]] = []
|
|
1598
|
+
nm = self.requested_order.node_metadata
|
|
1599
|
+
if nm.created_at:
|
|
1600
|
+
fields.append((METADATA_CREATED_AT, nm.created_at))
|
|
1601
|
+
if nm.updated_at:
|
|
1602
|
+
fields.append((METADATA_UPDATED_AT, nm.updated_at))
|
|
1603
|
+
return fields
|
|
1604
|
+
|
|
1605
|
+
@property
|
|
1606
|
+
def _has_metadata_filters(self) -> bool:
|
|
1607
|
+
"""Check if any metadata filters are requested."""
|
|
1608
|
+
if not self.filters:
|
|
1609
|
+
return False
|
|
1610
|
+
return any(key.startswith(NODE_METADATA_PREFIX) for key in self.filters)
|
|
1611
|
+
|
|
1125
1612
|
def _validate_filters(self) -> None:
|
|
1126
1613
|
if not self.filters:
|
|
1127
1614
|
return
|
|
@@ -1146,6 +1633,130 @@ class NodeGetListQuery(Query):
|
|
|
1146
1633
|
def _get_tracked_variables(self) -> list[str]:
|
|
1147
1634
|
return self._variables_to_track
|
|
1148
1635
|
|
|
1636
|
+
def _add_created_metadata_subquery(self, branch_filter: str) -> None:
|
|
1637
|
+
"""Add subquery to extract both created_at and created_by metadata.
|
|
1638
|
+
|
|
1639
|
+
Returns both values since they come from the same source (node properties or IS_PART_OF relationship).
|
|
1640
|
+
This subquery can be used for both filtering and ordering.
|
|
1641
|
+
"""
|
|
1642
|
+
tracked_vars = ", ".join(self._get_tracked_variables())
|
|
1643
|
+
|
|
1644
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1645
|
+
created_query = f"WITH {tracked_vars}, n.created_at AS created_at, n.created_by AS created_by"
|
|
1646
|
+
else:
|
|
1647
|
+
created_query = """
|
|
1648
|
+
CALL (n) {
|
|
1649
|
+
MATCH (:Node {uuid: n.uuid})-[r:IS_PART_OF {status: "active"}]->(:Root)
|
|
1650
|
+
WHERE %(branch_filter)s
|
|
1651
|
+
RETURN r.from AS created_at, r.from_user_id AS created_by
|
|
1652
|
+
ORDER BY r.from ASC
|
|
1653
|
+
LIMIT 1
|
|
1654
|
+
}
|
|
1655
|
+
WITH %(tracked_vars)s, created_at, created_by
|
|
1656
|
+
""" % {"branch_filter": branch_filter, "tracked_vars": tracked_vars}
|
|
1657
|
+
|
|
1658
|
+
self.add_to_query(created_query)
|
|
1659
|
+
self._track_variable("created_at")
|
|
1660
|
+
self._track_variable("created_by")
|
|
1661
|
+
|
|
1662
|
+
def _add_updated_metadata_subquery(self, branch_filter: str) -> None:
|
|
1663
|
+
"""Add subquery to extract both updated_at and updated_by metadata.
|
|
1664
|
+
|
|
1665
|
+
Returns both values since they come from the same source (node properties or attribute/relationship traversal).
|
|
1666
|
+
This subquery can be used for both filtering and ordering.
|
|
1667
|
+
"""
|
|
1668
|
+
tracked_vars = ", ".join(self._get_tracked_variables())
|
|
1669
|
+
|
|
1670
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1671
|
+
updated_query = f"WITH {tracked_vars}, n.updated_at AS updated_at, n.updated_by AS updated_by"
|
|
1672
|
+
else:
|
|
1673
|
+
if self.branch_agnostic:
|
|
1674
|
+
time_details = """
|
|
1675
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
1676
|
+
"""
|
|
1677
|
+
else:
|
|
1678
|
+
time_details = """
|
|
1679
|
+
WITH CASE
|
|
1680
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
1681
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
1682
|
+
ELSE [NULL, NULL]
|
|
1683
|
+
END AS from_details,
|
|
1684
|
+
CASE
|
|
1685
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
1686
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
1687
|
+
ELSE [NULL, NULL]
|
|
1688
|
+
END AS to_details
|
|
1689
|
+
"""
|
|
1690
|
+
|
|
1691
|
+
updated_query = """
|
|
1692
|
+
MATCH (n)-[r:HAS_ATTRIBUTE|IS_RELATED]-(field:Attribute|Relationship)
|
|
1693
|
+
WHERE %(branch_filter)s
|
|
1694
|
+
WITH DISTINCT %(tracked_vars)s, field
|
|
1695
|
+
CALL (field) {
|
|
1696
|
+
MATCH (field)-[r]-(property)
|
|
1697
|
+
WHERE %(branch_filter)s
|
|
1698
|
+
%(time_details)s
|
|
1699
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
1700
|
+
WITH from_details_list + to_details_list AS details_list
|
|
1701
|
+
UNWIND details_list AS one_details
|
|
1702
|
+
WITH one_details[0] AS updated_at_val, one_details[1] AS updated_by_val
|
|
1703
|
+
WHERE updated_at_val IS NOT NULL
|
|
1704
|
+
ORDER BY updated_at_val DESC
|
|
1705
|
+
LIMIT 1
|
|
1706
|
+
RETURN updated_at_val, updated_by_val
|
|
1707
|
+
}
|
|
1708
|
+
WITH %(tracked_vars)s, updated_at_val, updated_by_val
|
|
1709
|
+
ORDER BY elementId(n), updated_at_val DESC
|
|
1710
|
+
WITH %(tracked_vars)s,
|
|
1711
|
+
head(collect(updated_at_val)) AS updated_at,
|
|
1712
|
+
head(collect(updated_by_val)) AS updated_by
|
|
1713
|
+
""" % {"branch_filter": branch_filter, "time_details": time_details, "tracked_vars": tracked_vars}
|
|
1714
|
+
|
|
1715
|
+
self.add_to_query(updated_query)
|
|
1716
|
+
self._track_variable("updated_at")
|
|
1717
|
+
self._track_variable("updated_by")
|
|
1718
|
+
|
|
1719
|
+
def _add_metadata_subqueries(
|
|
1720
|
+
self,
|
|
1721
|
+
field_requirements: list[FieldAttributeRequirement],
|
|
1722
|
+
branch_filter: str,
|
|
1723
|
+
) -> None:
|
|
1724
|
+
"""Add unified subqueries for metadata filtering and ordering.
|
|
1725
|
+
|
|
1726
|
+
Uses a single subquery per metadata type (created or updated) that returns both
|
|
1727
|
+
_at and _by values, since they come from the same source. This is more efficient
|
|
1728
|
+
than separate subqueries for filtering and ordering.
|
|
1729
|
+
"""
|
|
1730
|
+
# Configuration for each metadata type: (allowed_fields, at_field, by_field, subquery_method)
|
|
1731
|
+
metadata_configs = [
|
|
1732
|
+
(METADATA_CREATED_FIELDS, METADATA_CREATED_AT, METADATA_CREATED_BY, self._add_created_metadata_subquery),
|
|
1733
|
+
(METADATA_UPDATED_FIELDS, METADATA_UPDATED_AT, METADATA_UPDATED_BY, self._add_updated_metadata_subquery),
|
|
1734
|
+
]
|
|
1735
|
+
|
|
1736
|
+
for allowed_fields, at_field, by_field, add_subquery in metadata_configs:
|
|
1737
|
+
requirements = [far for far in field_requirements if far.is_metadata and far.field_name in allowed_fields]
|
|
1738
|
+
if not requirements:
|
|
1739
|
+
continue
|
|
1740
|
+
|
|
1741
|
+
add_subquery(branch_filter)
|
|
1742
|
+
|
|
1743
|
+
is_first_filter = True
|
|
1744
|
+
for far in requirements:
|
|
1745
|
+
field = at_field if far.field_name == at_field else by_field
|
|
1746
|
+
|
|
1747
|
+
if far.is_metadata_filter:
|
|
1748
|
+
param_name = f"metadata_filter_{far.field_name}_{far.field_attr_name}_{far.index}"
|
|
1749
|
+
if is_first_filter:
|
|
1750
|
+
self.add_to_query(f"WHERE {field} {far.comparison_operator} ${param_name}")
|
|
1751
|
+
is_first_filter = False
|
|
1752
|
+
else:
|
|
1753
|
+
self.add_to_query(f"AND {field} {far.comparison_operator} ${param_name}")
|
|
1754
|
+
self.params[param_name] = far.field_attr_value
|
|
1755
|
+
|
|
1756
|
+
if far.is_metadata_order:
|
|
1757
|
+
direction = far.order_direction or OrderDirection.ASC
|
|
1758
|
+
self.order_by.append(f"{field} {direction.value}")
|
|
1759
|
+
|
|
1149
1760
|
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
1150
1761
|
self.order_by = []
|
|
1151
1762
|
|
|
@@ -1187,8 +1798,20 @@ class NodeGetListQuery(Query):
|
|
|
1187
1798
|
self.add_to_query(" AND n.uuid = $uuid")
|
|
1188
1799
|
return
|
|
1189
1800
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1801
|
+
# Determine ordering behavior
|
|
1802
|
+
disable_order = self.requested_order is not None and self.requested_order.disable
|
|
1803
|
+
has_any_order = bool(self.schema.order_by) or self._get_metadata_order_fields()
|
|
1804
|
+
|
|
1805
|
+
# needs ordering or filter if...
|
|
1806
|
+
needs_order_or_filter = bool(
|
|
1807
|
+
# any filters are set
|
|
1808
|
+
self.has_filters
|
|
1809
|
+
or self._has_metadata_filters
|
|
1810
|
+
# or any ordering is set and ordering is not disabled
|
|
1811
|
+
or (has_any_order and not disable_order)
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
if not needs_order_or_filter:
|
|
1192
1815
|
# Always order by uuid to guarantee pagination, see https://github.com/opsmill/infrahub/pull/4704.
|
|
1193
1816
|
self.order_by = ["n.uuid"]
|
|
1194
1817
|
return
|
|
@@ -1197,32 +1820,38 @@ class NodeGetListQuery(Query):
|
|
|
1197
1820
|
self.add_to_query("AND n.uuid IN $node_ids")
|
|
1198
1821
|
self.params["node_ids"] = self.filters["ids"]
|
|
1199
1822
|
|
|
1823
|
+
# Get unified field requirements for filtering and ordering
|
|
1200
1824
|
field_attribute_requirements = self._get_field_requirements(disable_order=disable_order)
|
|
1825
|
+
|
|
1826
|
+
is_default_or_global = self.branch.is_default or self.branch.is_global
|
|
1827
|
+
# Apply metadata subqueries first if default/global branch b/c they will be fast
|
|
1828
|
+
# Uses single subquery per metadata type for both filtering and ordering
|
|
1829
|
+
if is_default_or_global:
|
|
1830
|
+
self._add_metadata_subqueries(field_requirements=field_attribute_requirements, branch_filter=branch_filter)
|
|
1831
|
+
|
|
1832
|
+
# Apply regular attribute/relationship filter subqueries
|
|
1201
1833
|
await self._add_node_filter_attributes(
|
|
1202
1834
|
db=db, field_attribute_requirements=field_attribute_requirements, branch_filter=branch_filter
|
|
1203
1835
|
)
|
|
1204
1836
|
|
|
1205
|
-
if not
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1837
|
+
# Apply metadata subqueries last if not default/global branch b/c they will be slow
|
|
1838
|
+
if not is_default_or_global:
|
|
1839
|
+
self._add_metadata_subqueries(field_requirements=field_attribute_requirements, branch_filter=branch_filter)
|
|
1840
|
+
|
|
1841
|
+
# Apply order subqueries for non-metadata attributes (metadata ordering handled by _add_metadata_subqueries)
|
|
1842
|
+
await self._add_node_order_attributes(
|
|
1843
|
+
db=db, field_requirements=field_attribute_requirements, branch_filter=branch_filter
|
|
1844
|
+
)
|
|
1213
1845
|
|
|
1214
1846
|
# Always order by uuid to guarantee pagination, see https://github.com/opsmill/infrahub/pull/4704.
|
|
1215
1847
|
self.order_by.append("n.uuid")
|
|
1216
1848
|
|
|
1217
|
-
self._add_final_filter(field_attribute_requirements=field_attribute_requirements)
|
|
1218
|
-
|
|
1219
1849
|
async def _add_node_filter_attributes(
|
|
1220
1850
|
self,
|
|
1221
1851
|
db: InfrahubDatabase,
|
|
1222
1852
|
field_attribute_requirements: list[FieldAttributeRequirement],
|
|
1223
1853
|
branch_filter: str,
|
|
1224
1854
|
) -> None:
|
|
1225
|
-
field_attribute_requirements = [far for far in field_attribute_requirements if far.is_filter]
|
|
1226
1855
|
if not field_attribute_requirements:
|
|
1227
1856
|
return
|
|
1228
1857
|
|
|
@@ -1230,6 +1859,10 @@ class NodeGetListQuery(Query):
|
|
|
1230
1859
|
filter_params: dict[str, Any] = {}
|
|
1231
1860
|
|
|
1232
1861
|
for far in field_attribute_requirements:
|
|
1862
|
+
# Only process non-metadata filters; metadata filters are handled by _add_metadata_subqueries
|
|
1863
|
+
if not far.is_filter or far.is_metadata:
|
|
1864
|
+
continue
|
|
1865
|
+
|
|
1233
1866
|
extra_tail_properties = {far.node_value_query_variable: "value"}
|
|
1234
1867
|
subquery, subquery_params, subquery_result_name = await build_subquery_filter(
|
|
1235
1868
|
db=db,
|
|
@@ -1258,6 +1891,11 @@ class NodeGetListQuery(Query):
|
|
|
1258
1891
|
filter_query.append("}")
|
|
1259
1892
|
filter_query.append(f"WITH {with_str}")
|
|
1260
1893
|
|
|
1894
|
+
# Add WHERE clause immediately after the filter subquery for better performance
|
|
1895
|
+
where_clause = self._build_filter_where_clause(far)
|
|
1896
|
+
if where_clause:
|
|
1897
|
+
filter_query.append(where_clause)
|
|
1898
|
+
|
|
1261
1899
|
if filter_query:
|
|
1262
1900
|
self.add_to_query(filter_query)
|
|
1263
1901
|
self.params.update(filter_params)
|
|
@@ -1265,22 +1903,28 @@ class NodeGetListQuery(Query):
|
|
|
1265
1903
|
async def _add_node_order_attributes(
|
|
1266
1904
|
self,
|
|
1267
1905
|
db: InfrahubDatabase,
|
|
1268
|
-
|
|
1906
|
+
field_requirements: list[FieldAttributeRequirement],
|
|
1269
1907
|
branch_filter: str,
|
|
1270
1908
|
) -> None:
|
|
1271
|
-
|
|
1272
|
-
far for far in field_attribute_requirements if far.is_order and not far.is_filter
|
|
1273
|
-
]
|
|
1274
|
-
if not field_attribute_requirements:
|
|
1275
|
-
return
|
|
1909
|
+
"""Add ordering subqueries for schema attributes.
|
|
1276
1910
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1911
|
+
Note: Metadata ordering (created_at, updated_at) is handled by _add_metadata_subqueries.
|
|
1912
|
+
"""
|
|
1913
|
+
for far in field_requirements:
|
|
1914
|
+
# Skip metadata ordering - handled by _add_metadata_subqueries
|
|
1915
|
+
if far.is_metadata:
|
|
1916
|
+
continue
|
|
1279
1917
|
|
|
1280
|
-
|
|
1918
|
+
# Handle schema attribute ordering
|
|
1281
1919
|
if far.field is None:
|
|
1282
1920
|
continue
|
|
1283
1921
|
|
|
1922
|
+
# If this field is also used for filtering, the filter subquery already
|
|
1923
|
+
# extracted the value - just add it to order_by, don't create another subquery
|
|
1924
|
+
if far.is_filter:
|
|
1925
|
+
self.order_by.append(far.node_value_query_variable)
|
|
1926
|
+
continue
|
|
1927
|
+
|
|
1284
1928
|
subquery, subquery_params, _ = await build_subquery_order(
|
|
1285
1929
|
db=db,
|
|
1286
1930
|
field=far.field,
|
|
@@ -1294,96 +1938,243 @@ class NodeGetListQuery(Query):
|
|
|
1294
1938
|
self._track_variable(far.node_value_query_variable)
|
|
1295
1939
|
with_str = ", ".join(self._get_tracked_variables())
|
|
1296
1940
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
sort_query.append("}")
|
|
1301
|
-
sort_query.append(f"WITH {with_str}")
|
|
1941
|
+
self.params.update(subquery_params)
|
|
1942
|
+
self.add_to_query(["CALL (n) {", subquery, "}", f"WITH {with_str}"])
|
|
1943
|
+
self.order_by.append(far.node_value_query_variable)
|
|
1302
1944
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
self.params.update(sort_params)
|
|
1945
|
+
def _build_filter_where_clause(self, far: FieldAttributeRequirement) -> str | None:
|
|
1946
|
+
"""Build a WHERE clause for a single filter requirement.
|
|
1306
1947
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1948
|
+
Returns the WHERE clause string, or None if no clause is needed.
|
|
1949
|
+
"""
|
|
1950
|
+
if not far.is_filter or not far.is_attribute_value:
|
|
1951
|
+
return None
|
|
1952
|
+
|
|
1953
|
+
var_name = f"final_attr_value{far.index}"
|
|
1954
|
+
self.params[var_name] = far.field_attr_comparison_value
|
|
1955
|
+
|
|
1956
|
+
if self.partial_match:
|
|
1957
|
+
if isinstance(far.field_attr_comparison_value, list):
|
|
1958
|
+
# If the any filter is an array/list
|
|
1959
|
+
var_array = f"{var_name}_array"
|
|
1960
|
+
return f"WHERE any({var_array} IN ${var_name} WHERE toLower(toString({far.node_value_query_variable})) CONTAINS toLower({var_array}))"
|
|
1961
|
+
return f"WHERE toLower(toString({far.node_value_query_variable})) CONTAINS toLower(toString(${var_name}))"
|
|
1962
|
+
|
|
1963
|
+
if far.field and isinstance(far.field, AttributeSchema) and far.field.kind == "List":
|
|
1964
|
+
if isinstance(far.field_attr_comparison_value, list):
|
|
1965
|
+
self.params[var_name] = build_regex_attrs(values=far.field_attr_comparison_value)
|
|
1966
|
+
else:
|
|
1967
|
+
self.params[var_name] = build_regex_attrs(values=[far.field_attr_comparison_value])
|
|
1968
|
+
return f"WHERE toString({far.node_value_query_variable}) =~ ${var_name}"
|
|
1969
|
+
|
|
1970
|
+
return f"WHERE {far.node_value_query_variable} {far.comparison_operator} ${var_name}"
|
|
1971
|
+
|
|
1972
|
+
def _get_metadata_field_details(self, filter_key: str) -> tuple[str, str] | None:
|
|
1973
|
+
"""Parse a metadata filter key into field name and operator.
|
|
1974
|
+
|
|
1975
|
+
Args:
|
|
1976
|
+
filter_key: Filter key like "node_metadata__created_at__before"
|
|
1977
|
+
|
|
1978
|
+
Returns:
|
|
1979
|
+
Tuple of (field_name, operator) like ("created_at", "before"), or None if not a metadata filter.
|
|
1980
|
+
"""
|
|
1981
|
+
if not filter_key.startswith(NODE_METADATA_PREFIX):
|
|
1982
|
+
return None
|
|
1983
|
+
parts = filter_key.split("__")
|
|
1984
|
+
metadata_field_name = parts[1] # created_at, updated_at, created_by, updated_by
|
|
1985
|
+
metadata_operator = parts[2] if len(parts) > 2 else "value" # value, before, after, ids
|
|
1986
|
+
return metadata_field_name, metadata_operator
|
|
1987
|
+
|
|
1988
|
+
def _build_metadata_filter_requirement(
|
|
1989
|
+
self,
|
|
1990
|
+
field_name: str,
|
|
1991
|
+
operator: str,
|
|
1992
|
+
value: Any,
|
|
1993
|
+
index: int,
|
|
1994
|
+
) -> FieldAttributeRequirement:
|
|
1995
|
+
"""Build a FieldAttributeRequirement for a metadata filter."""
|
|
1996
|
+
if isinstance(value, datetime):
|
|
1997
|
+
timestamp = Timestamp(ZonedDateTime.from_py_datetime(value))
|
|
1998
|
+
value = timestamp.to_string()
|
|
1999
|
+
return FieldAttributeRequirement(
|
|
2000
|
+
field_name=field_name,
|
|
2001
|
+
field=None,
|
|
2002
|
+
field_attr_name=operator,
|
|
2003
|
+
field_attr_value=value,
|
|
2004
|
+
index=index,
|
|
2005
|
+
types=[FieldAttributeRequirementType.FILTER],
|
|
2006
|
+
is_metadata=True,
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
def _build_attribute_filter_requirement(
|
|
2010
|
+
self,
|
|
2011
|
+
field_name: str,
|
|
2012
|
+
field: AttributeSchema | RelationshipSchema | None,
|
|
2013
|
+
attr_name: str,
|
|
2014
|
+
attr_value: Any,
|
|
2015
|
+
index: int,
|
|
2016
|
+
) -> FieldAttributeRequirement:
|
|
2017
|
+
"""Build a FieldAttributeRequirement for an attribute/relationship filter."""
|
|
2018
|
+
return FieldAttributeRequirement(
|
|
2019
|
+
field_name=field_name,
|
|
2020
|
+
field=field,
|
|
2021
|
+
field_attr_name=attr_name,
|
|
2022
|
+
field_attr_value=attr_value.value if isinstance(attr_value, Enum) else attr_value,
|
|
2023
|
+
index=index,
|
|
2024
|
+
types=[FieldAttributeRequirementType.FILTER],
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
def _get_filter_requirements(self, start_index: int) -> list[FieldAttributeRequirement]:
|
|
2028
|
+
"""Build filter requirements from self.filters.
|
|
2029
|
+
|
|
2030
|
+
Processes both metadata and attribute/relationship filters in a single pass.
|
|
2031
|
+
Returns list of FieldAttributeRequirement objects.
|
|
2032
|
+
"""
|
|
2033
|
+
if not self.filters:
|
|
2034
|
+
return []
|
|
2035
|
+
|
|
2036
|
+
requirements: list[FieldAttributeRequirement] = []
|
|
2037
|
+
internal_filters = ["any", "attribute", "relationship"]
|
|
2038
|
+
processed_fields: set[str] = set()
|
|
2039
|
+
index = start_index
|
|
2040
|
+
|
|
2041
|
+
for filter_key in self.filters:
|
|
2042
|
+
# Check if this is a metadata filter
|
|
2043
|
+
metadata_details = self._get_metadata_field_details(filter_key)
|
|
2044
|
+
if metadata_details:
|
|
2045
|
+
field_name, operator = metadata_details
|
|
2046
|
+
requirements.append(
|
|
2047
|
+
self._build_metadata_filter_requirement(
|
|
2048
|
+
field_name=field_name,
|
|
2049
|
+
operator=operator,
|
|
2050
|
+
value=self.filters[filter_key],
|
|
2051
|
+
index=index,
|
|
1325
2052
|
)
|
|
2053
|
+
)
|
|
2054
|
+
index += 1
|
|
1326
2055
|
continue
|
|
1327
|
-
if far.field and isinstance(far.field, AttributeSchema) and far.field.kind == "List":
|
|
1328
|
-
if isinstance(far.field_attr_comparison_value, list):
|
|
1329
|
-
self.params[var_name] = build_regex_attrs(values=far.field_attr_comparison_value)
|
|
1330
|
-
else:
|
|
1331
|
-
self.params[var_name] = build_regex_attrs(values=[far.field_attr_comparison_value])
|
|
1332
2056
|
|
|
1333
|
-
|
|
2057
|
+
# Handle attribute/relationship filter
|
|
2058
|
+
# "height__value" -> "height"
|
|
2059
|
+
field_name = filter_key.split("__", maxsplit=1)[0]
|
|
2060
|
+
if field_name not in self.schema.valid_input_names + internal_filters:
|
|
1334
2061
|
continue
|
|
1335
2062
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
2063
|
+
# Skip if we've already processed this field (extract_field_filters handles all attrs for a field)
|
|
2064
|
+
if field_name in processed_fields:
|
|
2065
|
+
continue
|
|
2066
|
+
processed_fields.add(field_name)
|
|
1340
2067
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
for
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
continue
|
|
1350
|
-
field = self.schema.get_field(field_name, raise_on_error=False)
|
|
1351
|
-
for field_attr_name, field_attr_value in attr_filters.items():
|
|
1352
|
-
field_requirements_map[field_name, field_attr_name] = FieldAttributeRequirement(
|
|
2068
|
+
attr_filters = extract_field_filters(field_name=field_name, filters=self.filters)
|
|
2069
|
+
if not attr_filters:
|
|
2070
|
+
continue
|
|
2071
|
+
|
|
2072
|
+
field = self.schema.get_field(field_name, raise_on_error=False)
|
|
2073
|
+
for attr_name, attr_value in attr_filters.items():
|
|
2074
|
+
requirements.append(
|
|
2075
|
+
self._build_attribute_filter_requirement(
|
|
1353
2076
|
field_name=field_name,
|
|
1354
2077
|
field=field,
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
if isinstance(field_attr_value, Enum)
|
|
1358
|
-
else field_attr_value,
|
|
2078
|
+
attr_name=attr_name,
|
|
2079
|
+
attr_value=attr_value,
|
|
1359
2080
|
index=index,
|
|
1360
|
-
types=[FieldAttributeRequirementType.FILTER],
|
|
1361
2081
|
)
|
|
1362
|
-
|
|
2082
|
+
)
|
|
2083
|
+
index += 1
|
|
1363
2084
|
|
|
1364
|
-
|
|
1365
|
-
|
|
2085
|
+
return requirements
|
|
2086
|
+
|
|
2087
|
+
def _get_order_requirements(
|
|
2088
|
+
self,
|
|
2089
|
+
filter_requirements: list[FieldAttributeRequirement],
|
|
2090
|
+
start_index: int,
|
|
2091
|
+
) -> list[FieldAttributeRequirement]:
|
|
2092
|
+
"""Build ordering requirements.
|
|
2093
|
+
|
|
2094
|
+
Handles both metadata ordering and schema order_by.
|
|
2095
|
+
May modify existing requirements in filter_requirements to add ORDER type.
|
|
2096
|
+
Returns list of new FieldAttributeRequirement objects for order-only fields.
|
|
2097
|
+
"""
|
|
2098
|
+
# Build nested lookup map: field_name -> {field_attr_name -> requirement}
|
|
2099
|
+
requirements_map: dict[str | None, dict[str, FieldAttributeRequirement]] = {}
|
|
2100
|
+
for req in filter_requirements:
|
|
2101
|
+
if req.field_name not in requirements_map:
|
|
2102
|
+
requirements_map[req.field_name] = {}
|
|
2103
|
+
requirements_map[req.field_name][req.field_attr_name] = req
|
|
2104
|
+
|
|
2105
|
+
new_requirements: list[FieldAttributeRequirement] = []
|
|
2106
|
+
index = start_index
|
|
2107
|
+
|
|
2108
|
+
# Add metadata ordering requirements first
|
|
2109
|
+
for metadata_field, direction in self._get_metadata_order_fields():
|
|
2110
|
+
# Check if any filter exists for this metadata field
|
|
2111
|
+
field_reqs = requirements_map.get(metadata_field)
|
|
2112
|
+
existing_req = next(iter(field_reqs.values()), None) if field_reqs else None
|
|
2113
|
+
|
|
2114
|
+
if existing_req:
|
|
2115
|
+
# Field already used for filtering, add ORDER type
|
|
2116
|
+
existing_req.types.append(FieldAttributeRequirementType.ORDER)
|
|
2117
|
+
existing_req.order_direction = direction
|
|
2118
|
+
else:
|
|
2119
|
+
new_requirements.append(
|
|
2120
|
+
FieldAttributeRequirement(
|
|
2121
|
+
field_name=metadata_field,
|
|
2122
|
+
field=None,
|
|
2123
|
+
field_attr_name=metadata_field,
|
|
2124
|
+
field_attr_value=None,
|
|
2125
|
+
index=index,
|
|
2126
|
+
types=[FieldAttributeRequirementType.ORDER],
|
|
2127
|
+
order_direction=direction,
|
|
2128
|
+
is_metadata=True,
|
|
2129
|
+
)
|
|
2130
|
+
)
|
|
2131
|
+
index += 1
|
|
1366
2132
|
|
|
1367
|
-
|
|
2133
|
+
# Add schema order_by requirements
|
|
2134
|
+
for order_by_path in self.schema.order_by or []:
|
|
1368
2135
|
order_by_field_name, order_by_attr_property_name = order_by_path.split("__", maxsplit=1)
|
|
1369
2136
|
|
|
1370
2137
|
field = self.schema.get_field(order_by_field_name)
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
2138
|
+
field_reqs = requirements_map.get(order_by_field_name)
|
|
2139
|
+
existing_req = field_reqs.get(order_by_attr_property_name) if field_reqs else None
|
|
2140
|
+
if existing_req:
|
|
2141
|
+
# Field already used for filtering, add ORDER type
|
|
2142
|
+
existing_req.types.append(FieldAttributeRequirementType.ORDER)
|
|
2143
|
+
existing_req.order_direction = OrderDirection.ASC
|
|
2144
|
+
else:
|
|
2145
|
+
# New field requirement for ordering only
|
|
2146
|
+
new_requirements.append(
|
|
2147
|
+
FieldAttributeRequirement(
|
|
2148
|
+
field_name=order_by_field_name,
|
|
2149
|
+
field=field,
|
|
2150
|
+
field_attr_name=order_by_attr_property_name,
|
|
2151
|
+
field_attr_value=None,
|
|
2152
|
+
index=index,
|
|
2153
|
+
types=[FieldAttributeRequirementType.ORDER],
|
|
2154
|
+
order_direction=OrderDirection.ASC,
|
|
2155
|
+
)
|
|
2156
|
+
)
|
|
2157
|
+
index += 1
|
|
2158
|
+
|
|
2159
|
+
return new_requirements
|
|
2160
|
+
|
|
2161
|
+
def _get_field_requirements(self, disable_order: bool = False) -> list[FieldAttributeRequirement]:
|
|
2162
|
+
"""Build unified list of field requirements for filtering and ordering.
|
|
2163
|
+
|
|
2164
|
+
Iterates through filters once, using _get_metadata_field_details to determine
|
|
2165
|
+
whether each filter is metadata or attribute/relationship based.
|
|
2166
|
+
"""
|
|
2167
|
+
# Get filter requirements (single pass through self.filters)
|
|
2168
|
+
filter_requirements = self._get_filter_requirements(start_index=1)
|
|
2169
|
+
|
|
2170
|
+
if disable_order:
|
|
2171
|
+
return filter_requirements
|
|
2172
|
+
|
|
2173
|
+
# Get ordering requirements (may modify filter_requirements to add ORDER type)
|
|
2174
|
+
next_index = len(filter_requirements) + 1
|
|
2175
|
+
order_requirements = self._get_order_requirements(filter_requirements, start_index=next_index)
|
|
1385
2176
|
|
|
1386
|
-
return
|
|
2177
|
+
return filter_requirements + order_requirements
|
|
1387
2178
|
|
|
1388
2179
|
def get_node_ids(self) -> list[str]:
|
|
1389
2180
|
return [str(result.get("n.uuid")) for result in self.get_results()]
|