infrahub-server 1.7.0b0__py3-none-any.whl → 1.7.1__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/api/exceptions.py +2 -2
- infrahub/api/schema.py +5 -0
- infrahub/cli/db.py +54 -24
- infrahub/core/account.py +12 -9
- infrahub/core/branch/models.py +11 -117
- infrahub/core/branch/tasks.py +7 -3
- infrahub/core/diff/branch_differ.py +1 -1
- infrahub/core/diff/conflict_transferer.py +1 -1
- infrahub/core/diff/data_check_synchronizer.py +1 -1
- infrahub/core/diff/enricher/cardinality_one.py +1 -1
- infrahub/core/diff/enricher/hierarchy.py +1 -1
- infrahub/core/diff/enricher/labels.py +1 -1
- infrahub/core/diff/merger/merger.py +6 -2
- infrahub/core/diff/repository/repository.py +3 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +1 -1
- infrahub/core/initialization.py +2 -1
- infrahub/core/ipam/reconciler.py +8 -6
- infrahub/core/ipam/utilization.py +8 -15
- infrahub/core/manager.py +1 -26
- infrahub/core/merge.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -4
- infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
- infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
- infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
- infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
- infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
- infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
- infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +4 -3
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
- infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
- infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +19 -4
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
- infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
- infrahub/core/migrations/query/__init__.py +2 -2
- infrahub/core/migrations/query/schema_attribute_update.py +1 -1
- infrahub/core/migrations/runner.py +6 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
- infrahub/core/migrations/schema/attribute_name_update.py +1 -1
- infrahub/core/migrations/schema/attribute_supports_profile.py +5 -10
- infrahub/core/migrations/schema/models.py +8 -0
- infrahub/core/migrations/schema/node_attribute_add.py +11 -14
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
- infrahub/core/migrations/schema/node_kind_update.py +1 -1
- infrahub/core/migrations/schema/tasks.py +7 -1
- infrahub/core/migrations/shared.py +37 -30
- infrahub/core/node/__init__.py +3 -2
- infrahub/core/node/base.py +9 -5
- infrahub/core/node/delete_validator.py +1 -1
- infrahub/core/order.py +30 -0
- infrahub/core/protocols.py +1 -0
- infrahub/core/protocols_base.py +4 -0
- infrahub/core/query/__init__.py +8 -5
- infrahub/core/query/attribute.py +3 -3
- infrahub/core/query/branch.py +1 -1
- infrahub/core/query/delete.py +1 -1
- infrahub/core/query/diff.py +3 -3
- infrahub/core/query/ipam.py +104 -43
- infrahub/core/query/node.py +454 -101
- infrahub/core/query/relationship.py +83 -26
- infrahub/core/query/resource_manager.py +107 -18
- 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 +1 -1
- infrahub/core/relationship/model.py +8 -2
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +9 -15
- 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 +50 -38
- infrahub/core/schema/schema_branch.py +68 -2
- infrahub/core/utils.py +3 -3
- 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/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 +1 -1
- infrahub/git/utils.py +1 -1
- infrahub/graphql/app.py +2 -2
- infrahub/graphql/field_extractor.py +1 -1
- infrahub/graphql/manager.py +17 -3
- infrahub/graphql/mutations/account.py +1 -1
- infrahub/graphql/order.py +14 -0
- infrahub/graphql/queries/diff/tree.py +5 -5
- infrahub/graphql/queries/resource_manager.py +25 -24
- infrahub/graphql/resolvers/ipam.py +3 -3
- infrahub/graphql/resolvers/resolver.py +44 -3
- infrahub/graphql/types/standard_node.py +8 -4
- 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/node_applier.py +1 -1
- infrahub/profiles/queries/get_profile_data.py +1 -1
- infrahub/proposed_change/action_checker.py +1 -1
- infrahub/services/__init__.py +1 -1
- infrahub/services/adapters/cache/nats.py +1 -1
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/webhook/gather.py +1 -1
- infrahub/webhook/tasks.py +22 -6
- 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 +11 -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 +24 -1
- infrahub_sdk/node/relationship.py +10 -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.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/METADATA +6 -6
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/RECORD +176 -172
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/entry_points.txt +0 -1
- infrahub_testcontainers/models.py +3 -3
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/graphql/models.py +0 -36
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/core/query/node.py
CHANGED
|
@@ -5,12 +5,15 @@ 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
|
|
|
11
12
|
import ujson
|
|
13
|
+
from whenever import ZonedDateTime
|
|
12
14
|
|
|
13
15
|
from infrahub import config
|
|
16
|
+
from infrahub.constants.enums import OrderDirection
|
|
14
17
|
from infrahub.core import registry
|
|
15
18
|
from infrahub.core.constants import (
|
|
16
19
|
GLOBAL_BRANCH_NAME,
|
|
@@ -21,6 +24,13 @@ from infrahub.core.constants import (
|
|
|
21
24
|
RelationshipDirection,
|
|
22
25
|
RelationshipHierarchyDirection,
|
|
23
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
|
+
)
|
|
24
34
|
from infrahub.core.query import Query, QueryResult, QueryType
|
|
25
35
|
from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
|
|
26
36
|
from infrahub.core.query.utils import find_node_schema
|
|
@@ -28,7 +38,6 @@ from infrahub.core.schema.attribute_schema import AttributeSchema
|
|
|
28
38
|
from infrahub.core.timestamp import Timestamp
|
|
29
39
|
from infrahub.core.utils import build_regex_attrs, extract_field_filters
|
|
30
40
|
from infrahub.exceptions import QueryError
|
|
31
|
-
from infrahub.graphql.models import OrderModel
|
|
32
41
|
|
|
33
42
|
if TYPE_CHECKING:
|
|
34
43
|
from neo4j.graph import Node as Neo4jNode
|
|
@@ -44,6 +53,12 @@ if TYPE_CHECKING:
|
|
|
44
53
|
from infrahub.database import InfrahubDatabase
|
|
45
54
|
|
|
46
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
|
+
|
|
47
62
|
@dataclass
|
|
48
63
|
class NodeToProcess:
|
|
49
64
|
schema: NodeSchema | ProfileSchema | TemplateSchema | None
|
|
@@ -656,7 +671,7 @@ class NodeCheckIDQuery(Query):
|
|
|
656
671
|
self,
|
|
657
672
|
node_id: str,
|
|
658
673
|
**kwargs,
|
|
659
|
-
):
|
|
674
|
+
) -> None:
|
|
660
675
|
self.node_id = node_id
|
|
661
676
|
super().__init__(**kwargs)
|
|
662
677
|
|
|
@@ -688,7 +703,7 @@ class NodeListGetAttributeQuery(Query):
|
|
|
688
703
|
fields: dict | None = None,
|
|
689
704
|
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
690
705
|
**kwargs,
|
|
691
|
-
):
|
|
706
|
+
) -> None:
|
|
692
707
|
self.ids = ids
|
|
693
708
|
self.fields = fields
|
|
694
709
|
self.include_metadata = include_metadata
|
|
@@ -954,7 +969,7 @@ CALL (a) {
|
|
|
954
969
|
|
|
955
970
|
|
|
956
971
|
class GroupedPeerNodes:
|
|
957
|
-
def __init__(self):
|
|
972
|
+
def __init__(self) -> None:
|
|
958
973
|
# {node_id: [rel_name, ...]}
|
|
959
974
|
self._rel_names_by_node_id: dict[str, set[str]] = defaultdict(set)
|
|
960
975
|
# {(node_id, rel_name): {RelationshipDirection: {peer_id, ...}}}
|
|
@@ -1024,7 +1039,7 @@ class NodeListGetRelationshipsQuery(Query):
|
|
|
1024
1039
|
bidirectional_identifiers: list[str] | None = None,
|
|
1025
1040
|
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
1026
1041
|
**kwargs,
|
|
1027
|
-
):
|
|
1042
|
+
) -> None:
|
|
1028
1043
|
self.ids = ids
|
|
1029
1044
|
self.outbound_identifiers = outbound_identifiers
|
|
1030
1045
|
self.inbound_identifiers = inbound_identifiers
|
|
@@ -1486,6 +1501,9 @@ class FieldAttributeRequirement:
|
|
|
1486
1501
|
field_attr_value: Any
|
|
1487
1502
|
index: int
|
|
1488
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
|
|
1489
1507
|
|
|
1490
1508
|
@property
|
|
1491
1509
|
def is_attribute_value(self) -> bool:
|
|
@@ -1499,6 +1517,14 @@ class FieldAttributeRequirement:
|
|
|
1499
1517
|
def is_order(self) -> bool:
|
|
1500
1518
|
return FieldAttributeRequirementType.ORDER in self.types
|
|
1501
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
|
+
|
|
1502
1528
|
@property
|
|
1503
1529
|
def node_value_query_variable(self) -> str:
|
|
1504
1530
|
return f"attr{self.index}_node_value"
|
|
@@ -1507,8 +1533,12 @@ class FieldAttributeRequirement:
|
|
|
1507
1533
|
def comparison_operator(self) -> str:
|
|
1508
1534
|
if self.field_attr_name == "isnull":
|
|
1509
1535
|
return "=" if self.field_attr_value is True else "<>"
|
|
1510
|
-
if self.field_attr_name
|
|
1536
|
+
if self.field_attr_name in ("values", "ids"):
|
|
1511
1537
|
return "IN"
|
|
1538
|
+
if self.field_attr_name == "before":
|
|
1539
|
+
return "<"
|
|
1540
|
+
if self.field_attr_name == "after":
|
|
1541
|
+
return ">"
|
|
1512
1542
|
return "="
|
|
1513
1543
|
|
|
1514
1544
|
@property
|
|
@@ -1544,7 +1574,7 @@ class NodeGetListQuery(Query):
|
|
|
1544
1574
|
order = copy(order)
|
|
1545
1575
|
order.disable = True
|
|
1546
1576
|
|
|
1547
|
-
self.
|
|
1577
|
+
self.requested_order = order
|
|
1548
1578
|
|
|
1549
1579
|
super().__init__(**kwargs)
|
|
1550
1580
|
|
|
@@ -1560,6 +1590,25 @@ class NodeGetListQuery(Query):
|
|
|
1560
1590
|
return True
|
|
1561
1591
|
return False
|
|
1562
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
|
+
|
|
1563
1612
|
def _validate_filters(self) -> None:
|
|
1564
1613
|
if not self.filters:
|
|
1565
1614
|
return
|
|
@@ -1584,6 +1633,130 @@ class NodeGetListQuery(Query):
|
|
|
1584
1633
|
def _get_tracked_variables(self) -> list[str]:
|
|
1585
1634
|
return self._variables_to_track
|
|
1586
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
|
+
|
|
1587
1760
|
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
1588
1761
|
self.order_by = []
|
|
1589
1762
|
|
|
@@ -1625,8 +1798,20 @@ class NodeGetListQuery(Query):
|
|
|
1625
1798
|
self.add_to_query(" AND n.uuid = $uuid")
|
|
1626
1799
|
return
|
|
1627
1800
|
|
|
1628
|
-
|
|
1629
|
-
|
|
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:
|
|
1630
1815
|
# Always order by uuid to guarantee pagination, see https://github.com/opsmill/infrahub/pull/4704.
|
|
1631
1816
|
self.order_by = ["n.uuid"]
|
|
1632
1817
|
return
|
|
@@ -1635,32 +1820,38 @@ class NodeGetListQuery(Query):
|
|
|
1635
1820
|
self.add_to_query("AND n.uuid IN $node_ids")
|
|
1636
1821
|
self.params["node_ids"] = self.filters["ids"]
|
|
1637
1822
|
|
|
1823
|
+
# Get unified field requirements for filtering and ordering
|
|
1638
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
|
|
1639
1833
|
await self._add_node_filter_attributes(
|
|
1640
1834
|
db=db, field_attribute_requirements=field_attribute_requirements, branch_filter=branch_filter
|
|
1641
1835
|
)
|
|
1642
1836
|
|
|
1643
|
-
if not
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
+
)
|
|
1651
1845
|
|
|
1652
1846
|
# Always order by uuid to guarantee pagination, see https://github.com/opsmill/infrahub/pull/4704.
|
|
1653
1847
|
self.order_by.append("n.uuid")
|
|
1654
1848
|
|
|
1655
|
-
self._add_final_filter(field_attribute_requirements=field_attribute_requirements)
|
|
1656
|
-
|
|
1657
1849
|
async def _add_node_filter_attributes(
|
|
1658
1850
|
self,
|
|
1659
1851
|
db: InfrahubDatabase,
|
|
1660
1852
|
field_attribute_requirements: list[FieldAttributeRequirement],
|
|
1661
1853
|
branch_filter: str,
|
|
1662
1854
|
) -> None:
|
|
1663
|
-
field_attribute_requirements = [far for far in field_attribute_requirements if far.is_filter]
|
|
1664
1855
|
if not field_attribute_requirements:
|
|
1665
1856
|
return
|
|
1666
1857
|
|
|
@@ -1668,6 +1859,10 @@ class NodeGetListQuery(Query):
|
|
|
1668
1859
|
filter_params: dict[str, Any] = {}
|
|
1669
1860
|
|
|
1670
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
|
+
|
|
1671
1866
|
extra_tail_properties = {far.node_value_query_variable: "value"}
|
|
1672
1867
|
subquery, subquery_params, subquery_result_name = await build_subquery_filter(
|
|
1673
1868
|
db=db,
|
|
@@ -1696,6 +1891,11 @@ class NodeGetListQuery(Query):
|
|
|
1696
1891
|
filter_query.append("}")
|
|
1697
1892
|
filter_query.append(f"WITH {with_str}")
|
|
1698
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
|
+
|
|
1699
1899
|
if filter_query:
|
|
1700
1900
|
self.add_to_query(filter_query)
|
|
1701
1901
|
self.params.update(filter_params)
|
|
@@ -1703,22 +1903,28 @@ class NodeGetListQuery(Query):
|
|
|
1703
1903
|
async def _add_node_order_attributes(
|
|
1704
1904
|
self,
|
|
1705
1905
|
db: InfrahubDatabase,
|
|
1706
|
-
|
|
1906
|
+
field_requirements: list[FieldAttributeRequirement],
|
|
1707
1907
|
branch_filter: str,
|
|
1708
1908
|
) -> None:
|
|
1709
|
-
|
|
1710
|
-
far for far in field_attribute_requirements if far.is_order and not far.is_filter
|
|
1711
|
-
]
|
|
1712
|
-
if not field_attribute_requirements:
|
|
1713
|
-
return
|
|
1909
|
+
"""Add ordering subqueries for schema attributes.
|
|
1714
1910
|
|
|
1715
|
-
|
|
1716
|
-
|
|
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
|
|
1717
1917
|
|
|
1718
|
-
|
|
1918
|
+
# Handle schema attribute ordering
|
|
1719
1919
|
if far.field is None:
|
|
1720
1920
|
continue
|
|
1721
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
|
+
|
|
1722
1928
|
subquery, subquery_params, _ = await build_subquery_order(
|
|
1723
1929
|
db=db,
|
|
1724
1930
|
field=far.field,
|
|
@@ -1732,96 +1938,243 @@ class NodeGetListQuery(Query):
|
|
|
1732
1938
|
self._track_variable(far.node_value_query_variable)
|
|
1733
1939
|
with_str = ", ".join(self._get_tracked_variables())
|
|
1734
1940
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
sort_query.append("}")
|
|
1739
|
-
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)
|
|
1740
1944
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
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.
|
|
1744
1947
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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,
|
|
1763
2052
|
)
|
|
2053
|
+
)
|
|
2054
|
+
index += 1
|
|
1764
2055
|
continue
|
|
1765
|
-
if far.field and isinstance(far.field, AttributeSchema) and far.field.kind == "List":
|
|
1766
|
-
if isinstance(far.field_attr_comparison_value, list):
|
|
1767
|
-
self.params[var_name] = build_regex_attrs(values=far.field_attr_comparison_value)
|
|
1768
|
-
else:
|
|
1769
|
-
self.params[var_name] = build_regex_attrs(values=[far.field_attr_comparison_value])
|
|
1770
2056
|
|
|
1771
|
-
|
|
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:
|
|
1772
2061
|
continue
|
|
1773
2062
|
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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)
|
|
1778
2067
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
for
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
continue
|
|
1788
|
-
field = self.schema.get_field(field_name, raise_on_error=False)
|
|
1789
|
-
for field_attr_name, field_attr_value in attr_filters.items():
|
|
1790
|
-
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(
|
|
1791
2076
|
field_name=field_name,
|
|
1792
2077
|
field=field,
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
if isinstance(field_attr_value, Enum)
|
|
1796
|
-
else field_attr_value,
|
|
2078
|
+
attr_name=attr_name,
|
|
2079
|
+
attr_value=attr_value,
|
|
1797
2080
|
index=index,
|
|
1798
|
-
types=[FieldAttributeRequirementType.FILTER],
|
|
1799
2081
|
)
|
|
1800
|
-
|
|
2082
|
+
)
|
|
2083
|
+
index += 1
|
|
1801
2084
|
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
|
1804
2132
|
|
|
1805
|
-
|
|
2133
|
+
# Add schema order_by requirements
|
|
2134
|
+
for order_by_path in self.schema.order_by or []:
|
|
1806
2135
|
order_by_field_name, order_by_attr_property_name = order_by_path.split("__", maxsplit=1)
|
|
1807
2136
|
|
|
1808
2137
|
field = self.schema.get_field(order_by_field_name)
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
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)
|
|
1823
2176
|
|
|
1824
|
-
return
|
|
2177
|
+
return filter_requirements + order_requirements
|
|
1825
2178
|
|
|
1826
2179
|
def get_node_ids(self) -> list[str]:
|
|
1827
2180
|
return [str(result.get("n.uuid")) for result in self.get_results()]
|