infrahub-server 1.6.3__py3-none-any.whl → 1.7.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +4 -2
- infrahub/api/schema.py +3 -1
- infrahub/artifacts/tasks.py +1 -0
- infrahub/auth.py +2 -2
- infrahub/cli/db.py +6 -6
- 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 +5 -8
- 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/data_check_synchronizer.py +3 -2
- infrahub/core/diff/enricher/cardinality_one.py +1 -1
- infrahub/core/diff/merger/merger.py +27 -1
- 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/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +2 -2
- infrahub/core/graph/schema.py +2 -12
- infrahub/core/manager.py +132 -126
- 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/m013_convert_git_password_credential.py +3 -8
- 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/m049_remove_is_visible_relationship.py +38 -0
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
- 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/schema/attribute_kind_update.py +25 -7
- infrahub/core/migrations/schema/attribute_supports_profile.py +3 -1
- infrahub/core/migrations/schema/models.py +3 -0
- infrahub/core/migrations/schema/node_attribute_add.py +4 -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 +156 -57
- infrahub/core/node/create.py +7 -3
- infrahub/core/node/standard.py +100 -14
- infrahub/core/property.py +0 -1
- infrahub/core/protocols_base.py +6 -2
- infrahub/core/query/__init__.py +6 -7
- infrahub/core/query/attribute.py +161 -46
- infrahub/core/query/branch.py +57 -69
- infrahub/core/query/diff.py +4 -4
- infrahub/core/query/node.py +618 -180
- infrahub/core/query/relationship.py +449 -300
- infrahub/core/query/standard_node.py +25 -5
- infrahub/core/query/utils.py +2 -4
- infrahub/core/relationship/constraints/profiles_removal.py +168 -0
- infrahub/core/relationship/model.py +293 -139
- infrahub/core/schema/attribute_parameters.py +1 -28
- infrahub/core/schema/attribute_schema.py +17 -11
- infrahub/core/schema/manager.py +63 -43
- infrahub/core/schema/relationship_schema.py +6 -2
- infrahub/core/schema/schema_branch.py +48 -76
- infrahub/core/task/task.py +4 -2
- infrahub/core/utils.py +0 -22
- infrahub/core/validators/attribute/kind.py +2 -5
- infrahub/core/validators/determiner.py +3 -3
- infrahub/database/__init__.py +3 -3
- 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/graphql/app.py +2 -2
- infrahub/graphql/constants.py +3 -0
- infrahub/graphql/context.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 +158 -18
- infrahub/graphql/metadata.py +91 -0
- infrahub/graphql/models.py +33 -3
- infrahub/graphql/mutations/account.py +5 -5
- 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/queries/branch.py +62 -6
- infrahub/graphql/queries/diff/tree.py +5 -5
- 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 +16 -12
- 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 +4 -3
- infrahub/hfid/tasks.py +12 -3
- 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 +153 -12
- infrahub/profiles/queries/get_profile_data.py +143 -31
- infrahub/profiles/tasks.py +79 -27
- infrahub/profiles/triggers.py +22 -0
- infrahub/proposed_change/tasks.py +4 -1
- 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/models.py +1 -1
- infrahub/webhook/tasks.py +1 -1
- infrahub/workers/dependencies.py +9 -3
- infrahub/workers/infrahub_async.py +13 -4
- infrahub/workflows/catalogue.py +19 -0
- infrahub_sdk/node/constants.py +1 -0
- infrahub_sdk/node/related_node.py +13 -4
- infrahub_sdk/node/relationship.py +8 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/METADATA +17 -16
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/RECORD +161 -143
- infrahub_testcontainers/container.py +3 -3
- infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
- infrahub_testcontainers/docker-compose.test.yml +13 -5
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/core/query/node.py
CHANGED
|
@@ -8,6 +8,8 @@ from dataclasses import field as dataclass_field
|
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from typing import TYPE_CHECKING, Any, AsyncIterator, Generator
|
|
10
10
|
|
|
11
|
+
import ujson
|
|
12
|
+
|
|
11
13
|
from infrahub import config
|
|
12
14
|
from infrahub.core import registry
|
|
13
15
|
from infrahub.core.constants import (
|
|
@@ -15,6 +17,7 @@ from infrahub.core.constants import (
|
|
|
15
17
|
PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
|
|
16
18
|
PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER,
|
|
17
19
|
AttributeDBNodeType,
|
|
20
|
+
MetadataOptions,
|
|
18
21
|
RelationshipDirection,
|
|
19
22
|
RelationshipHierarchyDirection,
|
|
20
23
|
)
|
|
@@ -22,6 +25,7 @@ from infrahub.core.query import Query, QueryResult, QueryType
|
|
|
22
25
|
from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
|
|
23
26
|
from infrahub.core.query.utils import find_node_schema
|
|
24
27
|
from infrahub.core.schema.attribute_schema import AttributeSchema
|
|
28
|
+
from infrahub.core.timestamp import Timestamp
|
|
25
29
|
from infrahub.core.utils import build_regex_attrs, extract_field_filters
|
|
26
30
|
from infrahub.exceptions import QueryError
|
|
27
31
|
from infrahub.graphql.models import OrderModel
|
|
@@ -44,14 +48,15 @@ if TYPE_CHECKING:
|
|
|
44
48
|
class NodeToProcess:
|
|
45
49
|
schema: NodeSchema | ProfileSchema | TemplateSchema | None
|
|
46
50
|
|
|
51
|
+
labels: list[str]
|
|
47
52
|
node_id: str
|
|
48
53
|
node_uuid: str
|
|
49
|
-
|
|
50
|
-
updated_at: str
|
|
51
|
-
|
|
52
54
|
branch: str
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
created_at: Timestamp | None = None
|
|
57
|
+
created_by: str | None = None
|
|
58
|
+
updated_at: Timestamp | None = None
|
|
59
|
+
updated_by: str | None = None
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
@dataclass
|
|
@@ -74,13 +79,16 @@ class AttributeFromDB:
|
|
|
74
79
|
value: Any
|
|
75
80
|
content: Any
|
|
76
81
|
|
|
77
|
-
updated_at: str
|
|
78
|
-
|
|
79
82
|
branch: str
|
|
80
83
|
|
|
81
84
|
is_default: bool
|
|
82
85
|
is_from_profile: bool = dataclass_field(default=False)
|
|
83
86
|
|
|
87
|
+
updated_at: Timestamp | None = None
|
|
88
|
+
updated_by: str | None = None
|
|
89
|
+
created_at: Timestamp | None = None
|
|
90
|
+
created_by: str | None = None
|
|
91
|
+
|
|
84
92
|
node_properties: dict[str, AttributeNodePropertyFromDB] = dataclass_field(default_factory=dict)
|
|
85
93
|
flag_properties: dict[str, bool] = dataclass_field(default_factory=dict)
|
|
86
94
|
|
|
@@ -108,8 +116,6 @@ class NodeQuery(Query):
|
|
|
108
116
|
branch: Branch | None = None,
|
|
109
117
|
**kwargs,
|
|
110
118
|
) -> None:
|
|
111
|
-
# TODO Validate that Node is a valid node
|
|
112
|
-
# Eventually extract the branch from Node as well
|
|
113
119
|
self.node = node
|
|
114
120
|
self.node_id = node_id or id
|
|
115
121
|
self.node_db_id = node_db_id
|
|
@@ -133,6 +139,7 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
133
139
|
|
|
134
140
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002, PLR0915
|
|
135
141
|
at = self.at or self.node._at
|
|
142
|
+
self.params["user_id"] = self.user_id
|
|
136
143
|
self.params["uuid"] = self.node.id
|
|
137
144
|
self.params["branch"] = self.branch.name
|
|
138
145
|
self.params["branch_level"] = self.branch.hierarchy_level
|
|
@@ -188,7 +195,7 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
188
195
|
pass
|
|
189
196
|
except ValueError:
|
|
190
197
|
# 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
|
|
198
|
+
# typically because it will be allocated from a resource pool. In that case, the peer
|
|
192
199
|
# will be fetched using `rel.resolve` later.
|
|
193
200
|
pass
|
|
194
201
|
|
|
@@ -220,14 +227,40 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
220
227
|
"namespace": self.node._schema.namespace,
|
|
221
228
|
"branch_support": self.node._schema.branch,
|
|
222
229
|
}
|
|
230
|
+
if self.branch.is_default or self.branch.is_global:
|
|
231
|
+
self.params["node_prop"].update(
|
|
232
|
+
{
|
|
233
|
+
"created_at": at.to_string(),
|
|
234
|
+
"created_by": self.user_id,
|
|
235
|
+
"updated_at": at.to_string(),
|
|
236
|
+
"updated_by": self.user_id,
|
|
237
|
+
}
|
|
238
|
+
)
|
|
223
239
|
self.params["node_branch_prop"] = {
|
|
224
240
|
"branch": self.branch.name,
|
|
225
241
|
"branch_level": self.branch.hierarchy_level,
|
|
226
242
|
"status": "active",
|
|
227
243
|
"from": at.to_string(),
|
|
244
|
+
"from_user_id": self.user_id,
|
|
228
245
|
}
|
|
229
246
|
|
|
230
|
-
|
|
247
|
+
# set all the property strings that we reuse
|
|
248
|
+
# include the create/updated_at/by metadata if on default or global branch
|
|
249
|
+
attr_edge_prop_str = "{ branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at, from_user_id: $user_id }"
|
|
250
|
+
attr_vertex_prop_str = "{ uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support"
|
|
251
|
+
if self.branch.is_default or self.branch.is_global:
|
|
252
|
+
attr_vertex_prop_str += ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
|
|
253
|
+
attr_vertex_prop_str += " }"
|
|
254
|
+
|
|
255
|
+
rel_edge_prop_str = "{ branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at, from_user_id: $user_id }"
|
|
256
|
+
rel_edge_prop_str_hierarchy = (
|
|
257
|
+
"{ branch: rel.branch, branch_level: rel.branch_level, "
|
|
258
|
+
"status: rel.status, hierarchy: rel.hierarchical, from: $at, from_user_id: $user_id }"
|
|
259
|
+
)
|
|
260
|
+
rel_vertex_prop_str = "{ uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support"
|
|
261
|
+
if self.branch.is_default or self.branch.is_global:
|
|
262
|
+
rel_vertex_prop_str += ", created_at: $at, created_by: $user_id, updated_at: $at, updated_by: $user_id"
|
|
263
|
+
rel_vertex_prop_str += " }"
|
|
231
264
|
|
|
232
265
|
iphost_prop = {
|
|
233
266
|
"value": "attr.content.value",
|
|
@@ -270,102 +303,102 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
270
303
|
LIMIT 1
|
|
271
304
|
}
|
|
272
305
|
CALL (n, attr, av) {
|
|
273
|
-
CREATE (a:Attribute
|
|
274
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
275
|
-
CREATE (a)-[:HAS_VALUE
|
|
306
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
307
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
308
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
276
309
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
277
|
-
|
|
278
|
-
WITH a, ip, iv
|
|
310
|
+
WITH a, ip
|
|
279
311
|
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)
|
|
312
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
282
313
|
FOREACH ( prop IN attr.source_prop |
|
|
283
314
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
284
|
-
CREATE (a)-[:HAS_SOURCE
|
|
315
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
285
316
|
)
|
|
286
317
|
FOREACH ( prop IN attr.owner_prop |
|
|
287
318
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
288
|
-
CREATE (a)-[:HAS_OWNER
|
|
319
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
289
320
|
)
|
|
290
|
-
}"""
|
|
321
|
+
}""" % {"attr_edge": attr_edge_prop_str, "attr_vertex": attr_vertex_prop_str}
|
|
291
322
|
|
|
292
323
|
attrs_indexed_query = """
|
|
293
324
|
WITH distinct n
|
|
294
325
|
UNWIND $attrs_indexed AS attr
|
|
295
326
|
CALL (n, attr) {
|
|
296
|
-
CREATE (a:Attribute
|
|
297
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
327
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
328
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
298
329
|
MERGE (av:AttributeValue:AttributeValueIndexed { value: attr.content.value, is_default: attr.content.is_default })
|
|
299
330
|
WITH av, a
|
|
300
331
|
LIMIT 1
|
|
301
|
-
CREATE (a)-[:HAS_VALUE
|
|
332
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
302
333
|
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)
|
|
334
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
306
335
|
FOREACH ( prop IN attr.source_prop |
|
|
307
336
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
308
|
-
CREATE (a)-[:HAS_SOURCE
|
|
337
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
309
338
|
)
|
|
310
339
|
FOREACH ( prop IN attr.owner_prop |
|
|
311
340
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
312
|
-
CREATE (a)-[:HAS_OWNER
|
|
341
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
313
342
|
)
|
|
314
|
-
}"""
|
|
343
|
+
}""" % {"attr_edge": attr_edge_prop_str, "attr_vertex": attr_vertex_prop_str}
|
|
315
344
|
|
|
316
345
|
attrs_iphost_query = """
|
|
317
346
|
WITH distinct n
|
|
318
347
|
UNWIND $attrs_iphost AS attr
|
|
319
348
|
CALL (n, attr) {
|
|
320
|
-
CREATE (a:Attribute
|
|
321
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
349
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
350
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
322
351
|
MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPHost { %(iphost_prop)s })
|
|
323
352
|
WITH attr, av, a
|
|
324
353
|
LIMIT 1
|
|
325
|
-
CREATE (a)-[:HAS_VALUE
|
|
354
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
326
355
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
327
|
-
|
|
328
|
-
WITH a, ip, iv
|
|
356
|
+
WITH a, ip
|
|
329
357
|
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)
|
|
358
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
332
359
|
FOREACH ( prop IN attr.source_prop |
|
|
333
360
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
334
|
-
CREATE (a)-[:HAS_SOURCE
|
|
361
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
335
362
|
)
|
|
336
363
|
FOREACH ( prop IN attr.owner_prop |
|
|
337
364
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
338
|
-
CREATE (a)-[:HAS_OWNER
|
|
365
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
339
366
|
)
|
|
340
367
|
}
|
|
341
|
-
""" % {
|
|
368
|
+
""" % {
|
|
369
|
+
"iphost_prop": ", ".join(iphost_prop_list),
|
|
370
|
+
"attr_edge": attr_edge_prop_str,
|
|
371
|
+
"attr_vertex": attr_vertex_prop_str,
|
|
372
|
+
}
|
|
342
373
|
|
|
343
374
|
attrs_ipnetwork_query = """
|
|
344
375
|
WITH distinct n
|
|
345
376
|
UNWIND $attrs_ipnetwork AS attr
|
|
346
377
|
CALL (n, attr) {
|
|
347
|
-
CREATE (a:Attribute
|
|
348
|
-
CREATE (n)-[:HAS_ATTRIBUTE
|
|
378
|
+
CREATE (a:Attribute %(attr_vertex)s)
|
|
379
|
+
CREATE (n)-[:HAS_ATTRIBUTE %(attr_edge)s]->(a)
|
|
349
380
|
MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPNetwork { %(ipnetwork_prop)s })
|
|
350
381
|
WITH attr, av, a
|
|
351
382
|
LIMIT 1
|
|
352
|
-
CREATE (a)-[:HAS_VALUE
|
|
383
|
+
CREATE (a)-[:HAS_VALUE %(attr_edge)s]->(av)
|
|
353
384
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
354
|
-
|
|
355
|
-
WITH a, ip, iv
|
|
385
|
+
WITH a, ip
|
|
356
386
|
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)
|
|
387
|
+
CREATE (a)-[:IS_PROTECTED %(attr_edge)s]->(ip)
|
|
359
388
|
FOREACH ( prop IN attr.source_prop |
|
|
360
389
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
361
|
-
CREATE (a)-[:HAS_SOURCE
|
|
390
|
+
CREATE (a)-[:HAS_SOURCE %(attr_edge)s]->(peer)
|
|
362
391
|
)
|
|
363
392
|
FOREACH ( prop IN attr.owner_prop |
|
|
364
393
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
365
|
-
CREATE (a)-[:HAS_OWNER
|
|
394
|
+
CREATE (a)-[:HAS_OWNER %(attr_edge)s]->(peer)
|
|
366
395
|
)
|
|
367
396
|
}
|
|
368
|
-
""" % {
|
|
397
|
+
""" % {
|
|
398
|
+
"ipnetwork_prop": ", ".join(ipnetwork_prop_list),
|
|
399
|
+
"attr_edge": attr_edge_prop_str,
|
|
400
|
+
"attr_vertex": attr_vertex_prop_str,
|
|
401
|
+
}
|
|
369
402
|
|
|
370
403
|
deepest_branch = await registry.get_branch(db=db, branch=deepest_branch_name)
|
|
371
404
|
branch_filter, branch_params = deepest_branch.get_query_filter_path(at=self.at)
|
|
@@ -409,75 +442,84 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
409
442
|
UNWIND $rels_bidir AS rel
|
|
410
443
|
%(dest_node_subquery)s
|
|
411
444
|
CALL (n, rel, dest_node) {
|
|
412
|
-
CREATE (rl:Relationship
|
|
413
|
-
CREATE (n)-[:IS_RELATED %(
|
|
414
|
-
CREATE (dest_node)-[:IS_RELATED %(
|
|
445
|
+
CREATE (rl:Relationship %(rel_vertex)s)
|
|
446
|
+
CREATE (n)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
447
|
+
CREATE (dest_node)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
415
448
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
416
|
-
|
|
417
|
-
WITH rl, ip, iv
|
|
449
|
+
WITH rl, ip
|
|
418
450
|
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)
|
|
451
|
+
CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
|
|
421
452
|
FOREACH ( prop IN rel.source_prop |
|
|
422
453
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
423
|
-
CREATE (rl)-[:HAS_SOURCE
|
|
454
|
+
CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
|
|
424
455
|
)
|
|
425
456
|
FOREACH ( prop IN rel.owner_prop |
|
|
426
457
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
427
|
-
CREATE (rl)-[:HAS_OWNER
|
|
458
|
+
CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
|
|
428
459
|
)
|
|
429
460
|
}
|
|
430
|
-
""" % {
|
|
461
|
+
""" % {
|
|
462
|
+
"rel_edge": rel_edge_prop_str,
|
|
463
|
+
"rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
|
|
464
|
+
"rel_vertex": rel_vertex_prop_str,
|
|
465
|
+
"dest_node_subquery": dest_node_subquery,
|
|
466
|
+
}
|
|
431
467
|
|
|
432
468
|
rels_out_query = """
|
|
433
469
|
WITH distinct n
|
|
434
470
|
UNWIND $rels_out AS rel
|
|
435
471
|
%(dest_node_subquery)s
|
|
436
472
|
CALL (n, rel, dest_node) {
|
|
437
|
-
CREATE (rl:Relationship
|
|
438
|
-
CREATE (n)-[:IS_RELATED %(
|
|
439
|
-
CREATE (dest_node)<-[:IS_RELATED %(
|
|
473
|
+
CREATE (rl:Relationship %(rel_vertex)s)
|
|
474
|
+
CREATE (n)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
475
|
+
CREATE (dest_node)<-[:IS_RELATED %(rel_edge_hierarchy)s ]-(rl)
|
|
440
476
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
441
|
-
|
|
442
|
-
WITH rl, ip, iv
|
|
477
|
+
WITH rl, ip
|
|
443
478
|
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)
|
|
479
|
+
CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
|
|
446
480
|
FOREACH ( prop IN rel.source_prop |
|
|
447
481
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
448
|
-
CREATE (rl)-[:HAS_SOURCE
|
|
482
|
+
CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
|
|
449
483
|
)
|
|
450
484
|
FOREACH ( prop IN rel.owner_prop |
|
|
451
485
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
452
|
-
CREATE (rl)-[:HAS_OWNER
|
|
486
|
+
CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
|
|
453
487
|
)
|
|
454
488
|
}
|
|
455
|
-
""" % {
|
|
489
|
+
""" % {
|
|
490
|
+
"rel_edge": rel_edge_prop_str,
|
|
491
|
+
"rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
|
|
492
|
+
"rel_vertex": rel_vertex_prop_str,
|
|
493
|
+
"dest_node_subquery": dest_node_subquery,
|
|
494
|
+
}
|
|
456
495
|
|
|
457
496
|
rels_in_query = """
|
|
458
497
|
WITH distinct n
|
|
459
498
|
UNWIND $rels_in AS rel
|
|
460
499
|
%(dest_node_subquery)s
|
|
461
500
|
CALL (n, rel, dest_node) {
|
|
462
|
-
CREATE (rl:Relationship
|
|
463
|
-
CREATE (n)<-[:IS_RELATED %(
|
|
464
|
-
CREATE (dest_node)-[:IS_RELATED %(
|
|
501
|
+
CREATE (rl:Relationship %(rel_vertex)s)
|
|
502
|
+
CREATE (n)<-[:IS_RELATED %(rel_edge_hierarchy)s ]-(rl)
|
|
503
|
+
CREATE (dest_node)-[:IS_RELATED %(rel_edge_hierarchy)s ]->(rl)
|
|
465
504
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
466
|
-
|
|
467
|
-
WITH rl, ip, iv
|
|
505
|
+
WITH rl, ip
|
|
468
506
|
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)
|
|
507
|
+
CREATE (rl)-[:IS_PROTECTED %(rel_edge)s]->(ip)
|
|
471
508
|
FOREACH ( prop IN rel.source_prop |
|
|
472
509
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
473
|
-
CREATE (rl)-[:HAS_SOURCE
|
|
510
|
+
CREATE (rl)-[:HAS_SOURCE %(rel_edge)s]->(peer)
|
|
474
511
|
)
|
|
475
512
|
FOREACH ( prop IN rel.owner_prop |
|
|
476
513
|
MERGE (peer:Node { uuid: prop.peer_id })
|
|
477
|
-
CREATE (rl)-[:HAS_OWNER
|
|
514
|
+
CREATE (rl)-[:HAS_OWNER %(rel_edge)s]->(peer)
|
|
478
515
|
)
|
|
479
516
|
}
|
|
480
|
-
""" % {
|
|
517
|
+
""" % {
|
|
518
|
+
"rel_edge": rel_edge_prop_str,
|
|
519
|
+
"rel_edge_hierarchy": rel_edge_prop_str_hierarchy,
|
|
520
|
+
"rel_vertex": rel_vertex_prop_str,
|
|
521
|
+
"dest_node_subquery": dest_node_subquery,
|
|
522
|
+
}
|
|
481
523
|
|
|
482
524
|
query = f"""
|
|
483
525
|
MATCH (root:Root)
|
|
@@ -526,48 +568,83 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
526
568
|
|
|
527
569
|
class NodeDeleteQuery(NodeQuery):
|
|
528
570
|
name = "node_delete"
|
|
529
|
-
|
|
530
571
|
type: QueryType = QueryType.WRITE
|
|
531
|
-
|
|
532
|
-
raise_error_if_empty
|
|
572
|
+
insert_return = False
|
|
573
|
+
raise_error_if_empty = False
|
|
533
574
|
|
|
534
575
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
576
|
+
self.params["user_id"] = self.user_id
|
|
535
577
|
self.params["uuid"] = self.node_id
|
|
536
578
|
self.params["branch"] = self.branch.name
|
|
537
579
|
self.params["branch_level"] = self.branch.hierarchy_level
|
|
580
|
+
self.params["at"] = self.at.to_string()
|
|
538
581
|
|
|
539
582
|
if self.branch.is_global or self.branch.is_default:
|
|
583
|
+
# update the updated_at/by metadata on the Node if we're on the global or default branch
|
|
540
584
|
node_query_match = """
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
585
|
+
MATCH (n:Node { uuid: $uuid })-[r:IS_PART_OF { branch_level: 1, status: "active" }]->(:Root)
|
|
586
|
+
WHERE r.to IS NULL
|
|
587
|
+
OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
|
|
588
|
+
WHERE delete_edge.from <= $at
|
|
589
|
+
WITH n, r
|
|
590
|
+
WHERE delete_edge IS NULL
|
|
591
|
+
SET n.updated_at = $at, n.updated_by = $user_id
|
|
592
|
+
WITH n, r
|
|
545
593
|
"""
|
|
546
594
|
else:
|
|
547
595
|
node_filter, node_filter_params = self.branch.get_query_filter_path(at=self.at, variable_name="r")
|
|
548
596
|
node_query_match = """
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
597
|
+
MATCH (n:Node { uuid: $uuid })
|
|
598
|
+
CALL (n) {
|
|
599
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
600
|
+
WHERE %(node_filter)s
|
|
601
|
+
RETURN r
|
|
602
|
+
ORDER BY r.from DESC
|
|
603
|
+
LIMIT 1
|
|
604
|
+
}
|
|
605
|
+
WITH n, r
|
|
606
|
+
WHERE r.status = "active"
|
|
558
607
|
""" % {"node_filter": node_filter}
|
|
559
608
|
self.params.update(node_filter_params)
|
|
560
609
|
self.add_to_query(node_query_match)
|
|
561
610
|
|
|
611
|
+
# set the to time/user_id if the active IS_PART_OF edge is on this branch
|
|
562
612
|
query = """
|
|
563
|
-
|
|
564
|
-
|
|
613
|
+
MATCH (root:Root)
|
|
614
|
+
LIMIT 1
|
|
615
|
+
CREATE (n)-[delete_edge:IS_PART_OF { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at, from_user_id: $user_id }]->(root)
|
|
616
|
+
WITH r
|
|
617
|
+
WHERE r.branch = $branch
|
|
618
|
+
SET r.to = $at
|
|
619
|
+
SET r.to_user_id = $user_id
|
|
565
620
|
"""
|
|
621
|
+
self.add_to_query(query)
|
|
566
622
|
|
|
623
|
+
|
|
624
|
+
class NodeUpdateMetadataQuery(NodeQuery):
|
|
625
|
+
name = "node_update_metadata"
|
|
626
|
+
type: QueryType = QueryType.WRITE
|
|
627
|
+
insert_return = False
|
|
628
|
+
raise_error_if_empty = False
|
|
629
|
+
|
|
630
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
631
|
+
if not self.branch.is_default and not self.branch.is_global:
|
|
632
|
+
raise ValueError("NodeUpdateMetadataQuery can only be used on the default or global branch")
|
|
633
|
+
self.params["uuid"] = self.node_id
|
|
634
|
+
self.params["branch"] = self.branch.name
|
|
567
635
|
self.params["at"] = self.at.to_string()
|
|
636
|
+
self.params["user_id"] = self.user_id
|
|
568
637
|
|
|
638
|
+
query = """
|
|
639
|
+
MATCH (n:Node { uuid: $uuid })-[r:IS_PART_OF { branch_level: 1, status: "active" }]->(:Root)
|
|
640
|
+
WHERE r.to IS NULL
|
|
641
|
+
OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
|
|
642
|
+
WHERE delete_edge.from <= $at
|
|
643
|
+
WITH n, r
|
|
644
|
+
WHERE delete_edge IS NULL
|
|
645
|
+
SET n.updated_at = $at, n.updated_by = $user_id
|
|
646
|
+
"""
|
|
569
647
|
self.add_to_query(query)
|
|
570
|
-
self.return_labels = ["n"]
|
|
571
648
|
|
|
572
649
|
|
|
573
650
|
class NodeCheckIDQuery(Query):
|
|
@@ -603,30 +680,140 @@ class NodeListGetAttributeQuery(Query):
|
|
|
603
680
|
"HAS_OWNER": ("rel_owner", "owner"),
|
|
604
681
|
"HAS_SOURCE": ("rel_source", "source"),
|
|
605
682
|
"IS_PROTECTED": ("rel_isp", "isp"),
|
|
606
|
-
"IS_VISIBLE": ("rel_isv", "isv"),
|
|
607
683
|
}
|
|
608
684
|
|
|
609
685
|
def __init__(
|
|
610
686
|
self,
|
|
611
687
|
ids: list[str],
|
|
612
688
|
fields: dict | None = None,
|
|
613
|
-
|
|
614
|
-
include_owner: bool = False,
|
|
615
|
-
account=None,
|
|
689
|
+
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
616
690
|
**kwargs,
|
|
617
691
|
):
|
|
618
|
-
self.account = account
|
|
619
692
|
self.ids = ids
|
|
620
693
|
self.fields = fields
|
|
621
|
-
self.
|
|
622
|
-
self.include_owner = include_owner
|
|
623
|
-
|
|
694
|
+
self.include_metadata = include_metadata
|
|
624
695
|
super().__init__(order_by=["n.uuid", "a.name"], **kwargs)
|
|
625
696
|
|
|
697
|
+
@property
|
|
698
|
+
def _include_source(self) -> bool:
|
|
699
|
+
return bool(self.include_metadata & MetadataOptions.SOURCE)
|
|
700
|
+
|
|
701
|
+
@property
|
|
702
|
+
def _include_owner(self) -> bool:
|
|
703
|
+
return bool(self.include_metadata & MetadataOptions.OWNER)
|
|
704
|
+
|
|
705
|
+
@property
|
|
706
|
+
def _include_updated_metadata(self) -> bool:
|
|
707
|
+
return bool(self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY))
|
|
708
|
+
|
|
709
|
+
@property
|
|
710
|
+
def _include_created_metadata(self) -> bool:
|
|
711
|
+
return bool(self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY))
|
|
712
|
+
|
|
713
|
+
def _add_source_to_query(self, branch_filter_str: str) -> None:
|
|
714
|
+
if not self._include_source:
|
|
715
|
+
return
|
|
716
|
+
source_query = """
|
|
717
|
+
CALL (a) {
|
|
718
|
+
OPTIONAL MATCH (a)-[rel_source:HAS_SOURCE]-(source)
|
|
719
|
+
WHERE all(r IN [rel_source] WHERE ( %(branch_filter)s ))
|
|
720
|
+
RETURN source, rel_source
|
|
721
|
+
ORDER BY rel_source.branch_level DESC, rel_source.from DESC, rel_source.status ASC
|
|
722
|
+
LIMIT 1
|
|
723
|
+
}
|
|
724
|
+
WITH *,
|
|
725
|
+
CASE WHEN rel_source.status = "active" THEN source ELSE NULL END AS source,
|
|
726
|
+
CASE WHEN rel_source.status = "active" THEN rel_source ELSE NULL END AS rel_source
|
|
727
|
+
""" % {"branch_filter": branch_filter_str}
|
|
728
|
+
self.add_to_query(source_query)
|
|
729
|
+
self.return_labels.extend(["source", "rel_source"])
|
|
730
|
+
|
|
731
|
+
def _add_owner_to_query(self, branch_filter_str: str) -> None:
|
|
732
|
+
if not self._include_owner:
|
|
733
|
+
return
|
|
734
|
+
owner_query = """
|
|
735
|
+
CALL (a) {
|
|
736
|
+
OPTIONAL MATCH (a)-[rel_owner:HAS_OWNER]-(owner)
|
|
737
|
+
WHERE all(r IN [rel_owner] WHERE ( %(branch_filter)s ))
|
|
738
|
+
RETURN owner, rel_owner
|
|
739
|
+
ORDER BY rel_owner.branch_level DESC, rel_owner.from DESC, rel_owner.status ASC
|
|
740
|
+
LIMIT 1
|
|
741
|
+
}
|
|
742
|
+
WITH *,
|
|
743
|
+
CASE WHEN rel_owner.status = "active" THEN owner ELSE NULL END AS owner,
|
|
744
|
+
CASE WHEN rel_owner.status = "active" THEN rel_owner ELSE NULL END AS rel_owner
|
|
745
|
+
""" % {"branch_filter": branch_filter_str}
|
|
746
|
+
self.add_to_query(owner_query)
|
|
747
|
+
self.return_labels.extend(["owner", "rel_owner"])
|
|
748
|
+
|
|
749
|
+
def _add_created_metadata_to_query(self) -> None:
|
|
750
|
+
if not self._include_created_metadata:
|
|
751
|
+
return
|
|
752
|
+
if self.branch.is_default or self.branch.is_global:
|
|
753
|
+
last_created_query = """
|
|
754
|
+
WITH *, a.created_at AS created_at, a.created_by AS created_by
|
|
755
|
+
"""
|
|
756
|
+
else:
|
|
757
|
+
last_created_query = """
|
|
758
|
+
CALL (a) {
|
|
759
|
+
MATCH ()-[e:HAS_ATTRIBUTE {status: "active"}]->(a)
|
|
760
|
+
RETURN e.from AS created_at, e.from_user_id AS created_by
|
|
761
|
+
ORDER BY e.from ASC
|
|
762
|
+
LIMIT 1
|
|
763
|
+
}
|
|
764
|
+
"""
|
|
765
|
+
self.add_to_query(last_created_query)
|
|
766
|
+
self.return_labels.extend(["created_at", "created_by"])
|
|
767
|
+
|
|
768
|
+
def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
769
|
+
if not self._include_updated_metadata:
|
|
770
|
+
return
|
|
771
|
+
if self.branch.is_default or self.branch.is_global:
|
|
772
|
+
last_updated_query = """
|
|
773
|
+
WITH *, a.updated_at AS updated_at, a.updated_by AS updated_by
|
|
774
|
+
"""
|
|
775
|
+
else:
|
|
776
|
+
if self.branch_agnostic:
|
|
777
|
+
time_details = """
|
|
778
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
779
|
+
"""
|
|
780
|
+
else:
|
|
781
|
+
time_details = """
|
|
782
|
+
WITH CASE
|
|
783
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
784
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
785
|
+
ELSE [NULL, NULL]
|
|
786
|
+
END AS from_details,
|
|
787
|
+
CASE
|
|
788
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
789
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
790
|
+
ELSE [NULL, NULL]
|
|
791
|
+
END AS to_details
|
|
792
|
+
"""
|
|
793
|
+
last_updated_query = """
|
|
794
|
+
CALL (a) {
|
|
795
|
+
MATCH (a)-[r]->(property)
|
|
796
|
+
WHERE %(branch_filter)s
|
|
797
|
+
%(time_details)s
|
|
798
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
799
|
+
WITH from_details_list + to_details_list AS details_list
|
|
800
|
+
UNWIND details_list AS one_details
|
|
801
|
+
WITH one_details[0] AS updated_at, one_details[1] AS updated_by
|
|
802
|
+
WHERE updated_at IS NOT NULL
|
|
803
|
+
WITH updated_at, updated_by
|
|
804
|
+
ORDER BY updated_at DESC
|
|
805
|
+
LIMIT 1
|
|
806
|
+
RETURN updated_at, updated_by
|
|
807
|
+
}
|
|
808
|
+
""" % {"branch_filter": branch_filter_str, "time_details": time_details}
|
|
809
|
+
self.add_to_query(last_updated_query)
|
|
810
|
+
self.return_labels.extend(["updated_at", "updated_by"])
|
|
811
|
+
|
|
626
812
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
627
813
|
self.params["ids"] = self.ids
|
|
628
814
|
self.params["profile_node_relationship_name"] = PROFILE_NODE_RELATIONSHIP_IDENTIFIER
|
|
629
815
|
self.params["profile_template_relationship_name"] = PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER
|
|
816
|
+
self.params["field_names"] = list(self.fields.keys()) if self.fields else []
|
|
630
817
|
|
|
631
818
|
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
632
819
|
at=self.at, branch_agnostic=self.branch_agnostic
|
|
@@ -640,11 +827,9 @@ class NodeListGetAttributeQuery(Query):
|
|
|
640
827
|
exists((n)-[:IS_RELATED]-(:Relationship {name: $profile_template_relationship_name}))
|
|
641
828
|
) AS might_use_profile
|
|
642
829
|
MATCH (n)-[:HAS_ATTRIBUTE]-(a:Attribute)
|
|
830
|
+
WHERE (a.name IN $field_names OR size($field_names) = 0)
|
|
831
|
+
WITH DISTINCT n, a, might_use_profile
|
|
643
832
|
"""
|
|
644
|
-
if self.fields:
|
|
645
|
-
query += "\n WHERE a.name IN $field_names"
|
|
646
|
-
self.params["field_names"] = list(self.fields.keys())
|
|
647
|
-
|
|
648
833
|
self.add_to_query(query)
|
|
649
834
|
|
|
650
835
|
query = """
|
|
@@ -680,15 +865,8 @@ WHERE r2.status = "active"
|
|
|
680
865
|
|
|
681
866
|
self.return_labels = ["n", "a", "av", "r1", "r2", "is_from_profile"]
|
|
682
867
|
|
|
683
|
-
# Add Is_Protected
|
|
868
|
+
# Add Is_Protected
|
|
684
869
|
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
870
|
CALL (a) {
|
|
693
871
|
MATCH (a)-[r:IS_PROTECTED]-(isp:Boolean)
|
|
694
872
|
WHERE (%(branch_filter)s)
|
|
@@ -699,39 +877,12 @@ CALL (a) {
|
|
|
699
877
|
""" % {"branch_filter": branch_filter}
|
|
700
878
|
self.add_to_query(query)
|
|
701
879
|
|
|
702
|
-
self.return_labels.extend(["
|
|
880
|
+
self.return_labels.extend(["isp", "rel_isp"])
|
|
703
881
|
|
|
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"])
|
|
882
|
+
self._add_source_to_query(branch_filter_str=branch_filter)
|
|
883
|
+
self._add_owner_to_query(branch_filter_str=branch_filter)
|
|
884
|
+
self._add_created_metadata_to_query()
|
|
885
|
+
self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
|
|
735
886
|
|
|
736
887
|
def get_attributes_group_by_node(self) -> dict[str, NodeAttributesFromDB]:
|
|
737
888
|
attrs_by_node: dict[str, NodeAttributesFromDB] = {}
|
|
@@ -768,7 +919,6 @@ CALL (a) {
|
|
|
768
919
|
attr_uuid=attr.get("uuid"),
|
|
769
920
|
attr_value_id=attr_value.element_id,
|
|
770
921
|
attr_value_uuid=attr_value.get("uuid"),
|
|
771
|
-
updated_at=result.get_rel("r2").get("from"),
|
|
772
922
|
value=attr_value.get("value"),
|
|
773
923
|
is_default=attr_value.get("is_default"),
|
|
774
924
|
is_from_profile=is_from_profile,
|
|
@@ -776,16 +926,26 @@ CALL (a) {
|
|
|
776
926
|
branch=self.branch.name,
|
|
777
927
|
flag_properties={
|
|
778
928
|
"is_protected": result.get("isp").get("value"),
|
|
779
|
-
"is_visible": result.get("isv").get("value"),
|
|
780
929
|
},
|
|
781
930
|
)
|
|
782
931
|
|
|
783
|
-
if self.
|
|
932
|
+
if self.include_metadata & MetadataOptions.CREATED_AT:
|
|
933
|
+
created_at_str = result.get_as_str("created_at")
|
|
934
|
+
data.created_at = Timestamp(created_at_str) if created_at_str else None
|
|
935
|
+
if self.include_metadata & MetadataOptions.CREATED_BY:
|
|
936
|
+
data.created_by = result.get_as_str("created_by")
|
|
937
|
+
if self.include_metadata & MetadataOptions.UPDATED_AT:
|
|
938
|
+
updated_at_str = result.get_as_str("updated_at")
|
|
939
|
+
data.updated_at = Timestamp(updated_at_str) if updated_at_str else None
|
|
940
|
+
if self.include_metadata & MetadataOptions.UPDATED_BY:
|
|
941
|
+
data.updated_by = result.get_as_str("updated_by")
|
|
942
|
+
|
|
943
|
+
if self._include_source and result.get("source"):
|
|
784
944
|
data.node_properties["source"] = AttributeNodePropertyFromDB(
|
|
785
945
|
uuid=result.get_node("source").get("uuid"), labels=list(result.get_node("source").labels)
|
|
786
946
|
)
|
|
787
947
|
|
|
788
|
-
if self.
|
|
948
|
+
if self._include_owner and result.get("owner"):
|
|
789
949
|
data.node_properties["owner"] = AttributeNodePropertyFromDB(
|
|
790
950
|
uuid=result.get_node("owner").get("uuid"), labels=list(result.get_node("owner").labels)
|
|
791
951
|
)
|
|
@@ -799,12 +959,37 @@ class GroupedPeerNodes:
|
|
|
799
959
|
self._rel_names_by_node_id: dict[str, set[str]] = defaultdict(set)
|
|
800
960
|
# {(node_id, rel_name): {RelationshipDirection: {peer_id, ...}}}
|
|
801
961
|
self._rel_directions_map: dict[tuple[str, str], dict[RelationshipDirection, set[str]]] = defaultdict(dict)
|
|
962
|
+
# {(node_id, rel_name, direction, peer_Id): {MetadataOptions: value}}
|
|
963
|
+
self._metadata_map: dict[
|
|
964
|
+
tuple[str, str, RelationshipDirection, str], dict[MetadataOptions, Timestamp | str | None]
|
|
965
|
+
] = {}
|
|
802
966
|
|
|
803
|
-
def add_peer(
|
|
967
|
+
def add_peer(
|
|
968
|
+
self,
|
|
969
|
+
node_id: str,
|
|
970
|
+
rel_name: str,
|
|
971
|
+
peer_id: str,
|
|
972
|
+
direction: RelationshipDirection,
|
|
973
|
+
created_at: Timestamp | None = None,
|
|
974
|
+
created_by: str | None = None,
|
|
975
|
+
updated_at: Timestamp | None = None,
|
|
976
|
+
updated_by: str | None = None,
|
|
977
|
+
) -> None:
|
|
804
978
|
self._rel_names_by_node_id[node_id].add(rel_name)
|
|
805
979
|
if direction not in self._rel_directions_map[node_id, rel_name]:
|
|
806
980
|
self._rel_directions_map[node_id, rel_name][direction] = set()
|
|
807
981
|
self._rel_directions_map[node_id, rel_name][direction].add(peer_id)
|
|
982
|
+
key = (node_id, rel_name, direction, peer_id)
|
|
983
|
+
if created_at is not None or created_by is not None or updated_at is not None or updated_by is not None:
|
|
984
|
+
self._metadata_map[key] = {}
|
|
985
|
+
if created_at is not None:
|
|
986
|
+
self._metadata_map[key][MetadataOptions.CREATED_AT] = created_at
|
|
987
|
+
if created_by is not None:
|
|
988
|
+
self._metadata_map[key][MetadataOptions.CREATED_BY] = created_by
|
|
989
|
+
if updated_at is not None:
|
|
990
|
+
self._metadata_map[key][MetadataOptions.UPDATED_AT] = updated_at
|
|
991
|
+
if updated_by is not None:
|
|
992
|
+
self._metadata_map[key][MetadataOptions.UPDATED_BY] = updated_by
|
|
808
993
|
|
|
809
994
|
def get_peer_ids(self, node_id: str, rel_name: str, direction: RelationshipDirection) -> set[str]:
|
|
810
995
|
if (node_id, rel_name) not in self._rel_directions_map:
|
|
@@ -821,11 +1006,15 @@ class GroupedPeerNodes:
|
|
|
821
1006
|
def has_node(self, node_id: str) -> bool:
|
|
822
1007
|
return node_id in self._rel_names_by_node_id
|
|
823
1008
|
|
|
1009
|
+
def get_metadata_map(
|
|
1010
|
+
self, node_id: str, rel_name: str, direction: RelationshipDirection, peer_id: str
|
|
1011
|
+
) -> dict[MetadataOptions, Timestamp | str | None]:
|
|
1012
|
+
return self._metadata_map.get((node_id, rel_name, direction, peer_id), {})
|
|
1013
|
+
|
|
824
1014
|
|
|
825
1015
|
class NodeListGetRelationshipsQuery(Query):
|
|
826
1016
|
name: str = "node_list_get_relationship"
|
|
827
1017
|
type: QueryType = QueryType.READ
|
|
828
|
-
insert_return: bool = False
|
|
829
1018
|
|
|
830
1019
|
def __init__(
|
|
831
1020
|
self,
|
|
@@ -833,14 +1022,80 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
833
1022
|
outbound_identifiers: list[str] | None = None,
|
|
834
1023
|
inbound_identifiers: list[str] | None = None,
|
|
835
1024
|
bidirectional_identifiers: list[str] | None = None,
|
|
1025
|
+
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
836
1026
|
**kwargs,
|
|
837
1027
|
):
|
|
838
1028
|
self.ids = ids
|
|
839
1029
|
self.outbound_identifiers = outbound_identifiers
|
|
840
1030
|
self.inbound_identifiers = inbound_identifiers
|
|
841
1031
|
self.bidirectional_identifiers = bidirectional_identifiers
|
|
1032
|
+
self.include_metadata = include_metadata
|
|
842
1033
|
super().__init__(**kwargs)
|
|
843
1034
|
|
|
1035
|
+
def _add_created_metadata_to_query(self) -> None:
|
|
1036
|
+
if not (self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.CREATED_BY)):
|
|
1037
|
+
return
|
|
1038
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1039
|
+
last_created_query = """
|
|
1040
|
+
WITH *, rel.created_at AS created_at, rel.created_by AS created_by
|
|
1041
|
+
"""
|
|
1042
|
+
else:
|
|
1043
|
+
last_created_query = """
|
|
1044
|
+
WITH *, CASE
|
|
1045
|
+
WHEN r1.from < r2.from THEN [r1.from, r1.from_user_id]
|
|
1046
|
+
ELSE [r2.from, r2.from_user_id]
|
|
1047
|
+
END AS created_details
|
|
1048
|
+
WITH *, created_details[0] AS created_at, created_details[1] AS created_by
|
|
1049
|
+
"""
|
|
1050
|
+
self.add_to_query(last_created_query)
|
|
1051
|
+
self.return_labels.extend(["created_at", "created_by"])
|
|
1052
|
+
|
|
1053
|
+
def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
1054
|
+
if not (self.include_metadata & (MetadataOptions.UPDATED_AT | MetadataOptions.UPDATED_BY)):
|
|
1055
|
+
return
|
|
1056
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1057
|
+
last_updated_query = """
|
|
1058
|
+
WITH *, rel.updated_at AS updated_at, rel.updated_by AS updated_by
|
|
1059
|
+
"""
|
|
1060
|
+
else:
|
|
1061
|
+
if self.branch_agnostic:
|
|
1062
|
+
time_details = """
|
|
1063
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
1064
|
+
"""
|
|
1065
|
+
else:
|
|
1066
|
+
time_details = """
|
|
1067
|
+
WITH CASE
|
|
1068
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
1069
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
1070
|
+
ELSE [NULL, NULL]
|
|
1071
|
+
END AS from_details,
|
|
1072
|
+
CASE
|
|
1073
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
1074
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
1075
|
+
ELSE [NULL, NULL]
|
|
1076
|
+
END AS to_details
|
|
1077
|
+
"""
|
|
1078
|
+
last_updated_query = """
|
|
1079
|
+
CALL (rel) {
|
|
1080
|
+
// don't use IS_RELATED edges to handle the case when at least one of the
|
|
1081
|
+
// peers is a migrated-kind node
|
|
1082
|
+
MATCH (rel)-[r:!IS_RELATED]->(property)
|
|
1083
|
+
WHERE %(branch_filter)s
|
|
1084
|
+
%(time_details)s
|
|
1085
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
1086
|
+
WITH from_details_list + to_details_list AS details_list
|
|
1087
|
+
UNWIND details_list AS one_details
|
|
1088
|
+
WITH one_details[0] AS updated_at, one_details[1] AS updated_by
|
|
1089
|
+
WHERE updated_at IS NOT NULL
|
|
1090
|
+
WITH updated_at, updated_by
|
|
1091
|
+
ORDER BY updated_at DESC
|
|
1092
|
+
LIMIT 1
|
|
1093
|
+
RETURN updated_at, updated_by
|
|
1094
|
+
}
|
|
1095
|
+
""" % {"branch_filter": branch_filter_str, "time_details": time_details}
|
|
1096
|
+
self.add_to_query(last_updated_query)
|
|
1097
|
+
self.return_labels.extend(["updated_at", "updated_by"])
|
|
1098
|
+
|
|
844
1099
|
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
845
1100
|
self.params["ids"] = self.ids
|
|
846
1101
|
self.params["outbound_identifiers"] = self.outbound_identifiers
|
|
@@ -872,9 +1127,9 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
872
1127
|
LIMIT 1
|
|
873
1128
|
WITH r1, r AS r2
|
|
874
1129
|
WHERE r2.status = "active"
|
|
875
|
-
RETURN
|
|
1130
|
+
RETURN r1, r2
|
|
876
1131
|
}
|
|
877
|
-
RETURN n.uuid AS n_uuid, rel
|
|
1132
|
+
RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "inbound" as direction, r1, r2
|
|
878
1133
|
UNION
|
|
879
1134
|
WITH n
|
|
880
1135
|
MATCH (n)-[:IS_RELATED]->(rel:Relationship)-[:IS_RELATED]->(peer)
|
|
@@ -896,9 +1151,9 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
896
1151
|
LIMIT 1
|
|
897
1152
|
WITH r1, r AS r2
|
|
898
1153
|
WHERE r2.status = "active"
|
|
899
|
-
RETURN
|
|
1154
|
+
RETURN r1, r2
|
|
900
1155
|
}
|
|
901
|
-
RETURN n.uuid AS n_uuid, rel
|
|
1156
|
+
RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "outbound" as direction, r1, r2
|
|
902
1157
|
UNION
|
|
903
1158
|
WITH n
|
|
904
1159
|
MATCH (n)-[:IS_RELATED]->(rel:Relationship)<-[:IS_RELATED]-(peer)
|
|
@@ -920,15 +1175,21 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
920
1175
|
LIMIT 1
|
|
921
1176
|
WITH r1, r AS r2
|
|
922
1177
|
WHERE r2.status = "active"
|
|
923
|
-
RETURN
|
|
1178
|
+
RETURN r1, r2
|
|
924
1179
|
}
|
|
925
|
-
RETURN n.uuid AS n_uuid, rel
|
|
1180
|
+
RETURN n.uuid AS n_uuid, rel, peer.uuid AS peer_uuid, "bidirectional" as direction, r1, r2
|
|
926
1181
|
}
|
|
927
|
-
RETURN DISTINCT n_uuid, rel_name, peer_uuid, direction
|
|
928
1182
|
""" % {"filters": rels_filter}
|
|
929
1183
|
self.add_to_query(query)
|
|
1184
|
+
|
|
930
1185
|
self.order_by = ["n_uuid", "rel_name", "peer_uuid", "direction"]
|
|
931
|
-
self.return_labels = ["n_uuid", "
|
|
1186
|
+
self.return_labels = ["n_uuid", "peer_uuid", "direction"]
|
|
1187
|
+
|
|
1188
|
+
self._add_created_metadata_to_query()
|
|
1189
|
+
self._add_updated_metadata_to_query(branch_filter_str=rels_filter)
|
|
1190
|
+
return_labels_str = ", ".join(sorted(self.return_labels))
|
|
1191
|
+
self.add_to_query(f"WITH DISTINCT {return_labels_str}, rel.name AS rel_name")
|
|
1192
|
+
self.return_labels.append("rel_name")
|
|
932
1193
|
|
|
933
1194
|
def get_peers_group_by_node(self) -> GroupedPeerNodes:
|
|
934
1195
|
gpn = GroupedPeerNodes()
|
|
@@ -937,12 +1198,40 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
937
1198
|
rel_name = result.get("rel_name")
|
|
938
1199
|
peer_id = result.get("peer_uuid")
|
|
939
1200
|
direction = str(result.get("direction"))
|
|
1201
|
+
|
|
1202
|
+
created_at = None
|
|
1203
|
+
if self.include_metadata & MetadataOptions.CREATED_AT:
|
|
1204
|
+
created_at_str = result.get("created_at")
|
|
1205
|
+
created_at = Timestamp(created_at_str) if created_at_str else None
|
|
1206
|
+
|
|
1207
|
+
created_by_str = None
|
|
1208
|
+
if self.include_metadata & MetadataOptions.CREATED_BY:
|
|
1209
|
+
created_by_str = result.get("created_by")
|
|
1210
|
+
|
|
1211
|
+
updated_at = None
|
|
1212
|
+
if self.include_metadata & MetadataOptions.UPDATED_AT:
|
|
1213
|
+
updated_at_str = result.get("updated_at")
|
|
1214
|
+
updated_at = Timestamp(updated_at_str) if updated_at_str else None
|
|
1215
|
+
|
|
1216
|
+
updated_by_str = None
|
|
1217
|
+
if self.include_metadata & MetadataOptions.UPDATED_BY:
|
|
1218
|
+
updated_by_str = result.get("updated_by")
|
|
1219
|
+
|
|
940
1220
|
direction_enum = {
|
|
941
1221
|
"inbound": RelationshipDirection.INBOUND,
|
|
942
1222
|
"outbound": RelationshipDirection.OUTBOUND,
|
|
943
1223
|
"bidirectional": RelationshipDirection.BIDIR,
|
|
944
1224
|
}.get(direction)
|
|
945
|
-
gpn.add_peer(
|
|
1225
|
+
gpn.add_peer(
|
|
1226
|
+
node_id=node_id,
|
|
1227
|
+
rel_name=rel_name,
|
|
1228
|
+
peer_id=peer_id,
|
|
1229
|
+
direction=direction_enum,
|
|
1230
|
+
created_at=created_at,
|
|
1231
|
+
created_by=created_by_str,
|
|
1232
|
+
updated_at=updated_at,
|
|
1233
|
+
updated_by=updated_by_str,
|
|
1234
|
+
)
|
|
946
1235
|
|
|
947
1236
|
return gpn
|
|
948
1237
|
|
|
@@ -986,11 +1275,81 @@ class NodeListGetInfoQuery(Query):
|
|
|
986
1275
|
name = "node_list_get_info"
|
|
987
1276
|
type = QueryType.READ
|
|
988
1277
|
|
|
989
|
-
def __init__(self, ids: list[str],
|
|
990
|
-
self.account = account
|
|
1278
|
+
def __init__(self, ids: list[str], include_metadata: MetadataOptions = MetadataOptions.NONE, **kwargs: Any) -> None:
|
|
991
1279
|
self.ids = ids
|
|
1280
|
+
self.include_metadata = include_metadata
|
|
992
1281
|
super().__init__(**kwargs)
|
|
993
1282
|
|
|
1283
|
+
def _needs_user_timestamp_metadata(self) -> bool:
|
|
1284
|
+
return bool(self.include_metadata & MetadataOptions.USER_TIMESTAMPS)
|
|
1285
|
+
|
|
1286
|
+
def _add_created_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
1287
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1288
|
+
created_metadata_query = """
|
|
1289
|
+
WITH *, n.created_at AS created_at, n.created_by AS created_by
|
|
1290
|
+
"""
|
|
1291
|
+
else:
|
|
1292
|
+
created_metadata_query = """
|
|
1293
|
+
CALL (n) {
|
|
1294
|
+
MATCH (:Node {uuid: n.uuid})-[r:IS_PART_OF {status: "active"}]->(:Root)
|
|
1295
|
+
WHERE %(branch_filter)s
|
|
1296
|
+
RETURN r.from AS created_at, r.from_user_id AS created_by
|
|
1297
|
+
ORDER BY r.from ASC
|
|
1298
|
+
LIMIT 1
|
|
1299
|
+
}
|
|
1300
|
+
""" % {"branch_filter": branch_filter_str}
|
|
1301
|
+
self.add_to_query(created_metadata_query)
|
|
1302
|
+
self.return_labels.extend(["created_at", "created_by"])
|
|
1303
|
+
|
|
1304
|
+
def _add_updated_metadata_to_query(self, branch_filter_str: str) -> None:
|
|
1305
|
+
if self.branch.is_default or self.branch.is_global:
|
|
1306
|
+
last_update_query = """
|
|
1307
|
+
WITH *, n.updated_at AS updated_at, n.updated_by AS updated_by
|
|
1308
|
+
"""
|
|
1309
|
+
else:
|
|
1310
|
+
if self.branch_agnostic:
|
|
1311
|
+
time_details = """
|
|
1312
|
+
WITH [r.from, r.from_user_id] AS from_details, [r.to, r.to_user_id] AS to_details
|
|
1313
|
+
"""
|
|
1314
|
+
else:
|
|
1315
|
+
time_details = """
|
|
1316
|
+
WITH CASE
|
|
1317
|
+
WHEN r.branch IN $branch0 AND r.from < $time0 THEN [r.from, r.from_user_id]
|
|
1318
|
+
WHEN r.branch IN $branch1 AND r.from < $time1 THEN [r.from, r.from_user_id]
|
|
1319
|
+
ELSE [NULL, NULL]
|
|
1320
|
+
END AS from_details,
|
|
1321
|
+
CASE
|
|
1322
|
+
WHEN r.branch IN $branch0 AND r.to < $time0 THEN [r.to, r.to_user_id]
|
|
1323
|
+
WHEN r.branch IN $branch1 AND r.to < $time1 THEN [r.to, r.to_user_id]
|
|
1324
|
+
ELSE [NULL, NULL]
|
|
1325
|
+
END AS to_details
|
|
1326
|
+
"""
|
|
1327
|
+
last_update_query = """
|
|
1328
|
+
MATCH (n)-[r:HAS_ATTRIBUTE|IS_RELATED]-(field:Attribute|Relationship)
|
|
1329
|
+
WHERE %(branch_filter)s
|
|
1330
|
+
WITH DISTINCT n, r_is_part_of, field
|
|
1331
|
+
CALL (field) {
|
|
1332
|
+
MATCH (field)-[r]-(property)
|
|
1333
|
+
WHERE %(branch_filter)s
|
|
1334
|
+
%(time_details)s
|
|
1335
|
+
WITH collect(from_details) AS from_details_list, collect(to_details) AS to_details_list
|
|
1336
|
+
WITH from_details_list + to_details_list AS details_list
|
|
1337
|
+
UNWIND details_list AS one_details
|
|
1338
|
+
WITH one_details[0] AS updated_at, one_details[1] AS updated_by
|
|
1339
|
+
WHERE updated_at IS NOT NULL
|
|
1340
|
+
WITH updated_at, updated_by
|
|
1341
|
+
ORDER BY updated_at DESC
|
|
1342
|
+
LIMIT 1
|
|
1343
|
+
RETURN updated_at, updated_by
|
|
1344
|
+
}
|
|
1345
|
+
WITH n, r_is_part_of, updated_at, updated_by
|
|
1346
|
+
// updated_by ordering preferences non "__system__" users
|
|
1347
|
+
ORDER BY elementId(n), updated_at DESC, updated_by DESC
|
|
1348
|
+
WITH n, r_is_part_of, head(collect(updated_at)) AS updated_at, head(collect(updated_by)) AS updated_by
|
|
1349
|
+
""" % {"branch_filter": branch_filter_str, "time_details": time_details}
|
|
1350
|
+
self.add_to_query(last_update_query)
|
|
1351
|
+
self.return_labels.extend(["updated_at", "updated_by"])
|
|
1352
|
+
|
|
994
1353
|
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
995
1354
|
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
996
1355
|
at=self.at, branch_agnostic=self.branch_agnostic
|
|
@@ -1005,36 +1364,115 @@ class NodeListGetInfoQuery(Query):
|
|
|
1005
1364
|
CALL (root, n) {
|
|
1006
1365
|
MATCH (root:Root)<-[r:IS_PART_OF]-(n:Node)
|
|
1007
1366
|
WHERE %(branch_filter)s
|
|
1008
|
-
RETURN
|
|
1009
|
-
ORDER BY r.branch_level DESC, r.from DESC
|
|
1367
|
+
RETURN r AS r_is_part_of
|
|
1368
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
1010
1369
|
LIMIT 1
|
|
1011
1370
|
}
|
|
1012
|
-
WITH
|
|
1013
|
-
WHERE
|
|
1371
|
+
WITH n, r_is_part_of
|
|
1372
|
+
WHERE r_is_part_of.status = "active"
|
|
1014
1373
|
""" % {"branch_filter": branch_filter}
|
|
1015
|
-
|
|
1016
1374
|
self.add_to_query(query)
|
|
1375
|
+
self.return_labels = [
|
|
1376
|
+
"labels(n) AS node_labels",
|
|
1377
|
+
"r_is_part_of.branch AS branch",
|
|
1378
|
+
"elementId(n) AS node_database_id",
|
|
1379
|
+
"n.uuid AS node_uuid",
|
|
1380
|
+
]
|
|
1017
1381
|
|
|
1018
|
-
self.
|
|
1382
|
+
if self._needs_user_timestamp_metadata():
|
|
1383
|
+
self._add_updated_metadata_to_query(branch_filter_str=branch_filter)
|
|
1384
|
+
self._add_created_metadata_to_query(branch_filter_str=branch_filter)
|
|
1019
1385
|
|
|
1020
1386
|
async def get_nodes(self, db: InfrahubDatabase, duplicate: bool = False) -> AsyncIterator[NodeToProcess]:
|
|
1021
1387
|
"""Return all the node objects as NodeToProcess."""
|
|
1022
1388
|
|
|
1023
|
-
for result in self.
|
|
1024
|
-
|
|
1389
|
+
for result in self.get_results():
|
|
1390
|
+
raw_labels: list[str] = result.get_as_type(label="node_labels", return_type=list)
|
|
1391
|
+
labels = [str(lbl) for lbl in raw_labels]
|
|
1392
|
+
schema = find_node_schema(db=db, branch=self.branch, labels=labels, duplicate=duplicate)
|
|
1025
1393
|
node_branch = self.branch
|
|
1026
1394
|
if self.branch_agnostic:
|
|
1027
|
-
node_branch = result.
|
|
1395
|
+
node_branch = result.get_as_type(label="branch", return_type=str)
|
|
1396
|
+
|
|
1397
|
+
created_at = None
|
|
1398
|
+
created_by = None
|
|
1399
|
+
if self.include_metadata & (MetadataOptions.CREATED_AT | MetadataOptions.UPDATED_AT):
|
|
1400
|
+
raw_created_at = result.get_as_str(label="created_at")
|
|
1401
|
+
created_at = Timestamp(raw_created_at) if raw_created_at else None
|
|
1402
|
+
if self.include_metadata & (MetadataOptions.CREATED_BY | MetadataOptions.UPDATED_BY):
|
|
1403
|
+
created_by = result.get_as_str(label="created_by")
|
|
1404
|
+
updated_at = None
|
|
1405
|
+
updated_by = None
|
|
1406
|
+
if self.include_metadata & MetadataOptions.UPDATED_AT:
|
|
1407
|
+
raw_updated_at = result.get_as_str(label="updated_at")
|
|
1408
|
+
updated_at = Timestamp(raw_updated_at) if raw_updated_at else None
|
|
1409
|
+
if self.include_metadata & MetadataOptions.UPDATED_BY:
|
|
1410
|
+
updated_by = result.get_as_str(label="updated_by")
|
|
1411
|
+
|
|
1028
1412
|
yield NodeToProcess(
|
|
1029
1413
|
schema=schema,
|
|
1030
|
-
node_id=result.
|
|
1031
|
-
node_uuid=result.
|
|
1032
|
-
updated_at=result.get_rel("rb").get("from"),
|
|
1414
|
+
node_id=result.get_as_type(label="node_database_id", return_type=str),
|
|
1415
|
+
node_uuid=result.get_as_type(label="node_uuid", return_type=str),
|
|
1033
1416
|
branch=node_branch,
|
|
1034
|
-
labels=
|
|
1417
|
+
labels=labels,
|
|
1418
|
+
created_at=created_at,
|
|
1419
|
+
created_by=created_by,
|
|
1420
|
+
updated_at=updated_at or created_at,
|
|
1421
|
+
updated_by=updated_by or created_by,
|
|
1035
1422
|
)
|
|
1036
1423
|
|
|
1037
1424
|
|
|
1425
|
+
class NodeGetByHFIDQuery(Query):
|
|
1426
|
+
"""Query to lookup nodes by their HFID.
|
|
1427
|
+
|
|
1428
|
+
This query uses the stored `human_friendly_id` attribute on nodes.
|
|
1429
|
+
"""
|
|
1430
|
+
|
|
1431
|
+
name = "node_get_by_hfid"
|
|
1432
|
+
type = QueryType.READ
|
|
1433
|
+
|
|
1434
|
+
def __init__(self, node_kind: str, hfids: list[list[str]], **kwargs: Any) -> None:
|
|
1435
|
+
self.node_kind = node_kind
|
|
1436
|
+
self.hfids = hfids
|
|
1437
|
+
super().__init__(**kwargs)
|
|
1438
|
+
|
|
1439
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
1440
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
|
|
1441
|
+
self.params.update(branch_params)
|
|
1442
|
+
# The list is stored as a string in the database
|
|
1443
|
+
self.params["hfid_values"] = [ujson.dumps(hfid) for hfid in self.hfids]
|
|
1444
|
+
|
|
1445
|
+
query = """
|
|
1446
|
+
MATCH (n:%(node_kind)s)
|
|
1447
|
+
CALL (n) {
|
|
1448
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
1449
|
+
WHERE %(branch_filter)s
|
|
1450
|
+
RETURN r AS r_part_of
|
|
1451
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
1452
|
+
LIMIT 1
|
|
1453
|
+
}
|
|
1454
|
+
WITH n, r_part_of
|
|
1455
|
+
WHERE r_part_of.status = "active"
|
|
1456
|
+
MATCH (n)-[:HAS_ATTRIBUTE]->(attr:Attribute {name: "human_friendly_id"})
|
|
1457
|
+
CALL (attr) {
|
|
1458
|
+
MATCH (attr)-[r:HAS_VALUE]->(av)
|
|
1459
|
+
WHERE %(branch_filter)s
|
|
1460
|
+
RETURN av, r AS r_attr
|
|
1461
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
1462
|
+
LIMIT 1
|
|
1463
|
+
}
|
|
1464
|
+
WITH n, av, r_attr
|
|
1465
|
+
WHERE r_attr.status = "active" AND av.value IN $hfid_values
|
|
1466
|
+
""" % {"branch_filter": branch_filter, "node_kind": self.node_kind}
|
|
1467
|
+
|
|
1468
|
+
self.add_to_query(query)
|
|
1469
|
+
self.return_labels = ["n.uuid AS node_uuid", "av.value AS hfid"]
|
|
1470
|
+
|
|
1471
|
+
def get_node_uuids(self) -> list[str]:
|
|
1472
|
+
"""Get the list of node UUIDs from the query results."""
|
|
1473
|
+
return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
|
|
1474
|
+
|
|
1475
|
+
|
|
1038
1476
|
class FieldAttributeRequirementType(Enum):
|
|
1039
1477
|
FILTER = "filter"
|
|
1040
1478
|
ORDER = "order"
|